AuthDBFile.mm   [plain text]


/*
 * Copyright (c) 2003 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@
 */

#include <TargetConditionals.h>

extern "C" {
#import <stdio.h>
#import <string.h>
#import <sys/fcntl.h>
#import <sys/stat.h>
#import <syslog.h>
#import <unistd.h>
#import <LDAP/ldap.h>
#import "AuthDBFile.h"
#import "PSUtilitiesDefs.h"
#import "SASLCode.h"
#import "SMBAuth.h"
#import "ReplicaFileDefs.h"
#import "KerberosInterface.h"

#if COMPILE_WITH_RSA_LOAD
    #import "bufaux.h"
    #import "buffer.h"
    #import "cipher.h"
    #import "xmalloc.h"
    #import "ssh.h"
#endif
};

@implementation AuthDBFile

-(id)init
{
	self = [super init];
	mPWFileNO = -1;
	
	const char *altPathPrefix = getenv("PWSAltPathPrefix");
	if ( altPathPrefix != NULL )
	{
		char path[PATH_MAX];
		snprintf( path, sizeof(path), "%s/%s", altPathPrefix, kPWDirPath );
		mDirPathStr = strdup( path );
		snprintf( path, sizeof(path), "%s/%s", altPathPrefix, kPWFilePath );
		mFilePathStr = strdup( path );
	}
	else
	{
		mDirPathStr = strdup( kPWDirPath );
		mFilePathStr = strdup( kPWFilePath );
	}
	[self createOverflowObject];
	
	return self;
}

-(id)initWithFile:(const char *)inFilePath
{
	char *slash;
	
	self = [super init];
	mPWFileNO = -1;
	
	mFilePathStr = strdup( inFilePath );
	mDirPathStr = strdup( inFilePath );
	if ( mDirPathStr != NULL ) {
		slash = rindex( mDirPathStr, '/' );
		if ( slash != NULL )
			*slash = '\0';
	}
	
	[self createOverflowObject];
	return self;
}

-(void)createOverflowObject
{
	mOverflow = [AuthOverflowFile new];
}


-free
{	
	[self freeRSAKey];
	[self closePasswordFile];
	[self closeFreeListFile];
	
	if ( mFilePathStr != NULL )
		free( mFilePathStr );
	if ( mDirPathStr != NULL )
		free( mDirPathStr );
	if ( mSearchBase != NULL )
		free( mSearchBase );
	
	[mOverflow free];
	
	return [super free];
}


-(int)validateFiles
{
    int err;
    
    err = [self validatePasswordFile];
    if (err == 0)
		err = [self validateFreeListFile];
    
    return err;
}


//----------------------------------------------------------------------------------------------------
//	validatePasswordFile
//
//	Returns: file errors
//
//	Do internal checks on the database file.
//	Checks: signature, version, and size.
//----------------------------------------------------------------------------------------------------

-(int)validatePasswordFile
{
    int err;
    struct stat sb;
    PWFileHeader dbHeader;
	
    // validate
    err = lstat( mFilePathStr, &sb );
    if ( err == 0 )
		err = [self getHeader:&dbHeader];
    if ( err == 0 )
	{
        if ( mPWFile != NULL )
        {
            if ( mPWFileHeader.signature != kPWFileSignature ||
                 mPWFileHeader.version != kPWFileVersion ||
                 sb.st_size != (int32_t)(sizeof(mPWFileHeader) + mPWFileHeader.numberOfSlotsCurrentlyInFile * sizeof(PWFileEntry)) )
            {
                err = -1;
            }
        }
        else
        {
            err = -1;
        }
    }
    
    if ( err == 0 )
        mPWFileValidated = YES;
    
    return err;
}


-(int)validateFreeListFile
{
	return 0;
}


-(int)createPasswordFile
{
    int err = -1;
    size_t writeCount;
    const char *dirPathStr = mDirPathStr;
	const char *filePathStr = mFilePathStr;
	
	// make sure the directory exists
	err = pwsf_mkdir_p( dirPathStr, S_IRWXU );
	
	// if it existed before, double-check the permissions
	if ( err != 0 && errno == EEXIST )
		err = chmod( dirPathStr, S_IRWXU );
	
    // create new file
    mPWFile = fopen( filePathStr, "w+" );
    if ( mPWFile != NULL )
    {
		mPWFileNO = fileno( mPWFile );
        err = chmod( filePathStr, S_IRUSR | S_IWUSR );
        if ( err == -1 )
            err = errno;
        // ignore
        err = 0;
        
        // set header initial state
        bzero( &mPWFileHeader, sizeof(PWFileHeader) );
        mPWFileHeader.signature = kPWFileSignature;
        mPWFileHeader.version = kPWFileVersion;
		mPWFileHeader.entrySize = sizeof(PWFileEntry);
        mPWFileHeader.sequenceNumber = 0;
        mPWFileHeader.numberOfSlotsCurrentlyInFile = kPWFileInitialSlots;
        mPWFileHeader.deepestSlotUsed = 0;
        mPWFileHeader.deepestSlotUsedByThisServer = 0;
		
        mPWFileHeader.access.usingHistory = 0;
        mPWFileHeader.access.usingExpirationDate = 0;
        mPWFileHeader.access.usingHardExpirationDate = 0;
        mPWFileHeader.access.requiresAlpha = 0;
        mPWFileHeader.access.requiresNumeric = 0;
        mPWFileHeader.access.passwordIsHash = 0;
		
        // do not need to set these if usingExpirationDate and usingHardExpirationDate are false
        //mPWFileHeader.access.expirationDateGMT
        //mPWFileHeader.access.hardExpireDateGMT
    
        mPWFileHeader.access.maxMinutesUntilChangePassword = 0;
        mPWFileHeader.access.maxMinutesUntilDisabled = 0;	
        mPWFileHeader.access.maxMinutesOfNonUse = 0;
        mPWFileHeader.access.maxFailedLoginAttempts = 0;
        mPWFileHeader.access.minChars = 0;
        mPWFileHeader.access.maxChars = 0;
		time( (time_t *)&mPWFileHeader.accessModDate );
		
        // write header
		err = [self setHeader:&mPWFileHeader];
		
        // write blank space
        if ( err == 0 )
        {
            PWFileEntry anEntry;
            int i;
            
            bzero( &anEntry, sizeof(PWFileEntry) );
            
            for ( i = kPWFileInitialSlots; i > 0; i-- )
            {
                writeCount = fwrite( &anEntry, sizeof(PWFileEntry), 1, mPWFile );
                if ( writeCount != 1 )
                {
                    err = -1;
                    break;
                }
            }
        }
        fflush( mPWFile );
        
        if ( err == 0 )
			[self validateFiles];
		else
            unlink( filePathStr );
    }
    else
    {
        if ( errno )
            err = errno;
    }
    
    return err;
}


//----------------------------------------------------------------------------------------------------
//	openPasswordFile
//
//	Returns: file errors
//	Does not Return: file mapping errors.
//
//	Utility function to open the database. When the database is opened with write-access,
//	the file is not mapped to keep it immediately up-to-date. For read-only, the file is
//	mapped by all except getPasswordRec. Access is switched to write-access for each AUTH,
//	so there is no advantage to mapping.
//----------------------------------------------------------------------------------------------------

-(int)openPasswordFile:(const char *)mode
{
    int err = 0;
     
    if ( mPWFile != NULL && strcmp( mode, mPWFilePermission ) == 0 )
    {
        return err;
    }
    else
    {
        [self closePasswordFile];
        
        mPWFile = fopen( mFilePathStr, mode );
		
		// handle read-only file system
		if ( mPWFile == NULL && errno == EROFS )
		{
			mReadOnlyFileSystem = YES;
			mPWFile = fopen( mFilePathStr, "r" );
		}
		
        if ( mPWFile != NULL )
        {
		mPWFileNO = fileno( mPWFile );
            strcpy( mPWFilePermission, mReadOnlyFileSystem ? "r" : mode );
        }
        else
        {
            err = errno;
            if ( err == 0 )
                err = -1;
        }
    }
    
    return err;
}


-(void)closePasswordFile
{
	[self pwWait];
	if ( mPWFile != NULL ) {
		if ( mDBFileLocked )
			[self pwUnlock];
		
		fclose( mPWFile );
		mPWFile = NULL;
		mPWFileNO = -1;
    }
	mGotHeader = NO;
	[self pwSignal];
}


-(void)closeFreeListFile
{
	[self pwWait];
    if ( mFreeListFile != NULL ) {
        fclose( mFreeListFile );
        mFreeListFile = NULL;
    }
	[self pwSignal];
}


-(void)freeRSAKey
{
	[self rsaWait];
	if ( rsaKey != NULL ) {
		RSA_free( rsaKey );
		rsaKey = NULL;
	}
	[self rsaSignal];
}


-(void)resetPasswordFileState
{
    if ( mPWFile )
        fflush( mPWFile );
    
    [self closePasswordFile];
	
	// force the rsa key to be reloaded
    [self freeRSAKey];
}


-(void)pwLock
{
	int tries = 3;
	int result;
	
	if ( mPWFile != NULL )
	{
		while ( (result = flock( mPWFileNO, LOCK_EX | LOCK_NB )) == -1 && tries-- > 0 )
			usleep( 25000 );
		
		if ( result == 0 )
			mDBFileLocked = YES;
	}
}


//----------------------------------------------------------------------------------------------------
//	pwLock
//
//	Returns: YES if the lock is obtained.
//----------------------------------------------------------------------------------------------------

-(BOOL)pwLock:(unsigned long)inMillisecondsToWait
{
	const long millisecondsPerTry = 25;
	long tries = inMillisecondsToWait / millisecondsPerTry;
	BOOL locked = NO;
	
	if ( mPWFile == NULL )
	{
		mDBFileLocked = NO;
		[self openPasswordFile:"r+"];
    }
	
	if ( mPWFile != NULL )
	{
		if ( mDBFileLocked )
			return YES;
		
		if ( tries <= 0 )
			tries = 1;
		
		while ( tries-- > 0 )
		{
			if ( flock( mPWFileNO, LOCK_EX | LOCK_NB ) == 0 )
			{
				locked = YES;
				break;
			}
			
			usleep( millisecondsPerTry * 1000 );
		}
	}
	
	mDBFileLocked = locked;
	return locked;
}


-(void)pwUnlock
{
	if ( mPWFile != NULL )
		flock( mPWFileNO, LOCK_UN );
	mDBFileLocked = NO;
}


-(void)pwWait
{
	// override in sub-class
}


-(void)pwSignal
{
	// override in sub-class
}


-(void)rsaWait
{
	// override in sub-class
}


-(void)rsaSignal
{
	// override in sub-class
}


//----------------------------------------------------------------------------------------------------
//	getHeader
//
//	Returns: 0=success, -1=fail, -2=recovery failed, -3 recovery used
//----------------------------------------------------------------------------------------------------

-(int)getHeader:(PWFileHeader *)outHeader
{
	return [self getHeader:outHeader cachedCopyOK:NO];
}


-(int)getHeader:(PWFileHeader *)outHeader cachedCopyOK:(BOOL)inCanUseCachedCopy
{
    int err = -1;
    ssize_t readCount;
    BOOL saveAfterReleasingSemaphore = NO;
	
    if ( outHeader == NULL )
        return -1;
    
	if ( inCanUseCachedCopy && mGotHeader )
	{
		memcpy( outHeader, &mPWFileHeader, sizeof(PWFileHeader) );
		return 0;
	}
	
    [self pwWait];
	err = [self openPasswordFile:mReadOnlyFileSystem ? "r" : "r+"];
    if ( err == 0 && mPWFile )
    {
		readCount = pread( mPWFileNO, outHeader, sizeof(PWFileHeader), 0 );
        pwsf_EndianAdjustPWFileHeader( outHeader, 1 );
		
		if ( outHeader->signature == kPWFileSignature )
		{
			// adopt the new header data
			memcpy( &mPWFileHeader, outHeader, sizeof(PWFileHeader) );
		}
		else
		{
			err = -2;
			
			// bad news, try to recover
			if ( mGotHeader && mPWFileHeader.signature == kPWFileSignature )
			{
				err = -3;
				
				memcpy( outHeader, &mPWFileHeader, sizeof(PWFileHeader) );
				saveAfterReleasingSemaphore = YES;
			}
		}
		
		mGotHeader = YES;
    }
    [self pwSignal];
    
	if ( saveAfterReleasingSemaphore )
		[self setHeader:&mPWFileHeader];
	
    return err;
}


//----------------------------------------------------------------------------------------------------
//	setHeader
//
//	Returns: 0=success, -1=fail
//----------------------------------------------------------------------------------------------------

-(int)setHeader:(const PWFileHeader *)inHeader
{
    int err = -1;
    long writeCount;
    
    if ( inHeader == NULL )
        return -1;
	if ( inHeader->signature != kPWFileSignature )
		return -1;
	if ( mReadOnlyFileSystem )
		return -1;
	
    [self pwWait];
    err = [self openPasswordFile:"r+"];
    if ( err == 0 && mPWFile )
    {
        err = fseek( mPWFile, 0, SEEK_SET );
        if ( err == 0 )
        {
            // adopt the new header data
			if ( inHeader != &mPWFileHeader )
				memcpy( &mPWFileHeader, inHeader, sizeof(PWFileHeader) );
            
            // write to disk
#if TARGET_RT_LITTLE_ENDIAN
			PWFileHeader diskHeader = mPWFileHeader;
			pwsf_EndianAdjustPWFileHeader( &diskHeader, 0 );
            writeCount = fwrite( &diskHeader, sizeof(PWFileHeader), 1, mPWFile );
			bzero( &diskHeader, sizeof(PWFileHeader) );
#else
            writeCount = fwrite( &mPWFileHeader, sizeof(PWFileHeader), 1, mPWFile );
#endif
            if ( writeCount != 1 )
            {
                err = -1;
            }
            fflush( mPWFile );
        }
    }
	[self pwSignal];
    
    return err;
}


//----------------------------------------------------------------------------------------------------
//	getRSAPublicKey
//
//	Returns a base64 encoded rsa key
//----------------------------------------------------------------------------------------------------

-(int)getRSAPublicKey:(char *)outRSAKeyStr
{
    PWFileHeader dbHeader;
    int result = 0;
    long len;
	
    if ( outRSAKeyStr == NULL )
        return -1;
    *outRSAKeyStr = '\0';
    
	result = [self getHeader:&dbHeader cachedCopyOK:YES];
    if ( result == 0 || result == -3 )
	{
        strncpy(outRSAKeyStr, (char *)dbHeader.publicKey, kPWFileMaxPublicKeyBytes);
		
		// strip linefeed from the end
		len = strlen(outRSAKeyStr);
		if ( len > 0 && outRSAKeyStr[len-1] == '\n' )
			outRSAKeyStr[len-1] = '\0';
	}
	
    bzero(&dbHeader, sizeof(dbHeader));
    
    return result;
}


//----------------------------------------------------------------------------------------------------
//	loadRSAKeys
//
//	Returns: -1=no code, 0=no key, 1=success
//
//	loads the key blob from the database header into a struct that can be used with
//	BSD RSA functions.
//----------------------------------------------------------------------------------------------------

-(int)loadRSAKeys
{
    int result = 0;
	unsigned int idx;
	
#if COMPILE_WITH_RSA_LOAD

    PWFileHeader dbHeader;
    char passphrase[1] = "";
    
    // check if we already loaded the key
	[self rsaWait];
   	if ( rsaKey != NULL )
	{
		[self rsaSignal];
		return 1;
    }
	
	result = [self getHeader:&dbHeader cachedCopyOK:YES];
    if ( result == 0 || result == -3 )
    {
        int check1, check2, cipher_type;
        off_t len;
        Buffer buffer, decrypted;
        char *cp;
        CipherContext cipher;
        BN_CTX *ctx;
        BIGNUM *aux;
        time_t now;
		
        len = dbHeader.privateKeyLen;
        
        buffer_init(&buffer);
        buffer_append_space(&buffer, &cp, len);
        
        memcpy(cp, dbHeader.privateKey, len);
        
        /* Check that it is at least big enought to contain the ID string. */
        if ( len < (off_t)sizeof(AUTHFILE_ID_STRING) ) {
            syslog(LOG_INFO, "Bad key.");
            buffer_free(&buffer);
			
			[self rsaSignal];
            return 0;
        }
        /*
        * Make sure it begins with the id string.  Consume the id string
        * from the buffer.
        */
        for (idx = 0; idx < (unsigned int) sizeof(AUTHFILE_ID_STRING); idx++)
            if (buffer_get_char(&buffer) != (unsigned char) AUTHFILE_ID_STRING[idx]) {
                syslog(LOG_ALERT, "Bad key.");
				buffer_free(&buffer);
				
				[self rsaSignal];
                return 0;
            }
        /* Read cipher type. */
        cipher_type = buffer_get_char(&buffer);
        (void) buffer_get_int(&buffer);	/* Reserved data. */
    
        /* Read the public key from the buffer. */
        buffer_get_int(&buffer);
        rsaKey = RSA_new();
		if ( rsaKey == NULL ) {
			[self rsaSignal];
			return 0;
		}
        rsaKey->n = BN_new();
        buffer_get_bignum(&buffer, rsaKey->n);
        rsaKey->e = BN_new();
        buffer_get_bignum(&buffer, rsaKey->e);
        //if (comment_return)
        //    *comment_return = buffer_get_string(&buffer, NULL);
        //else
            xfree(buffer_get_string(&buffer, NULL));
    
        /* Check that it is a supported cipher. */
        if (((cipher_mask1() | SSH_CIPHER_NONE | SSH_AUTHFILE_CIPHER) & (1 << cipher_type)) == 0) {
            syslog(LOG_INFO, "Unsupported cipher %.100s used in key.", cipher_name(cipher_type));
            buffer_free(&buffer);
            goto fail;
        }
        /* Initialize space for decrypted data. */
        buffer_init(&decrypted);
        buffer_append_space(&decrypted, &cp, buffer_len(&buffer));
    
        /* Rest of the buffer is encrypted.  Decrypt it using the passphrase. */
        cipher_set_key_string(&cipher, cipher_type, passphrase);
        cipher_decrypt(&cipher, (unsigned char *) cp,
                (unsigned char *) buffer_ptr(&buffer),
                buffer_len(&buffer));
    
        buffer_free(&buffer);
    
        check1 = buffer_get_char(&decrypted);
        check2 = buffer_get_char(&decrypted);
        if (check1 != buffer_get_char(&decrypted) ||
            check2 != buffer_get_char(&decrypted)) {
            if (strcmp(passphrase, "") != 0)
                syslog(LOG_INFO, "Bad passphrase supplied for key.");
            /* Bad passphrase. */
            buffer_free(&decrypted);
    fail:
            RSA_free(rsaKey);
			rsaKey = NULL;
			[self rsaSignal];
            return 0;
        }
        /* Read the rest of the private key. */
        rsaKey->d = BN_new();
		if (rsaKey->d == NULL) {
			RSA_free(rsaKey);
			rsaKey = NULL;
			[self rsaSignal];
            return 0;
		}
        buffer_get_bignum(&decrypted, rsaKey->d);
        rsaKey->iqmp = BN_new();
        buffer_get_bignum(&decrypted, rsaKey->iqmp);	/* u */
        /* in SSL and SSH p and q are exchanged */
        rsaKey->q = BN_new();
        buffer_get_bignum(&decrypted, rsaKey->q);		/* p */
        rsaKey->p = BN_new();
        buffer_get_bignum(&decrypted, rsaKey->p);		/* q */
    
        ctx = BN_CTX_new();
        aux = BN_new();
    
        BN_sub(aux, rsaKey->q, BN_value_one());
        rsaKey->dmq1 = BN_new();
        BN_mod(rsaKey->dmq1, rsaKey->d, aux, ctx);
    
        BN_sub(aux, rsaKey->p, BN_value_one());
        rsaKey->dmp1 = BN_new();
        BN_mod(rsaKey->dmp1, rsaKey->d, aux, ctx);
    
        BN_clear_free(aux);
        BN_CTX_free(ctx);
    
        buffer_free(&decrypted);
    
		time(&now);
		srand((int)now);
		if ( RSA_blinding_on( rsaKey, NULL ) != 1 )
			syslog( LOG_INFO, "could not enable RSA_blinding" );
		
		bzero(&dbHeader, sizeof(dbHeader));
		
		[self rsaSignal];
        return 1;
	}
    
    bzero(&dbHeader, sizeof(dbHeader));
    [self rsaSignal];
	
#else
    syslog(LOG_ALERT, "RSA key loading not compiled\n");
    result = -1;
#endif

    return result;
}


//----------------------------------------------------------------------------------------------------
//	decryptRSA
//
//	Returns: -1=fail, 0=success
//----------------------------------------------------------------------------------------------------

-(int)decryptRSA:(unsigned char *)inBlob length:(int)inBlobLen result:(unsigned char *)outBlob
{
    int len;
    int result = 0;
	
	if ( [self loadRSAKeys] != 1 )
        return -1;
    
	[self rsaWait];
	len = RSA_private_decrypt( inBlobLen, inBlob, outBlob, rsaKey, RSA_PKCS1_PADDING );
	[self rsaSignal];
	
	if (len <= 0)
	{
		// print the error for debugging only. The error code may apply to Klima-Pokomy-Rosa attack.
		//syslog( LOG_INFO, "rsa_private_decrypt() failed, err = %lu", ERR_get_error() );
        syslog( LOG_ALERT, "rsa_private_decrypt() failed" );
        result = -1;
		
		// let's try reloading the key
		[self freeRSAKey];
		
		if ( [self loadRSAKeys] == 1 )
		{
			[self rsaWait];
			len = RSA_private_decrypt( inBlobLen, inBlob, outBlob, rsaKey, RSA_PKCS1_PADDING );
			[self rsaSignal];
			
			if ( len > 0 )
				result = 0;
		}
    }
	
    return result;
}


//----------------------------------------------------------------------------------------------------
//	encryptRSA
//
//	Returns: -1=fail, or length of encrypted part of <outBlob>
//----------------------------------------------------------------------------------------------------

-(int)encryptRSA:(unsigned char *)inBlob length:(int)inBlobLen result:(unsigned char *)outBlob
{
    int len;
    int maxRSASize;
	
    if ( [self loadRSAKeys] != 1 )
        return -1;
    
	// the maximum length of a block when using RSA_PKCS1_PADDING
	// is RSA_size( rsaKey ) - 11 (see the man page for RSA_public_encrypt)
	maxRSASize = RSA_size( rsaKey );
	if ( inBlobLen > maxRSASize - 11 )
		inBlobLen = maxRSASize - 11;
	
	[self rsaWait];
	len = RSA_public_encrypt( inBlobLen, inBlob, outBlob, rsaKey, RSA_PKCS1_PADDING );
	[self rsaSignal];
	
    if ( len <= 0 ) {
		//pwsf_fatal("RSA_public_encrypt() failed");
        return -1;
    }
    
	outBlob[len] = '\0';
	
    return len;
}


//----------------------------------------------------------------------------------------------------
//	isWeakAuthMethod
//
//	Returns: Boolean (0 == NO, 1 == YES)
//
//	A "weak" authentication method is one that is not secure enough to allow administration.
//	Generally, methods like CRYPT and PLAIN are not trusted because they are replayable.
//	CRAM and similar methods are trusted because a brute-force attack would take some time.
//----------------------------------------------------------------------------------------------------

-(int)isWeakAuthMethod:(const char *)inMethod
{
    int index;
    int result;
	PWFileHeader dbHeader;
    
	result = [self getHeader:&dbHeader cachedCopyOK:YES];
	if ( result != 0 && result != -3 )
        return 1;
    
    for ( index = 0; index < kPWFileMaxWeakMethods; index++ )
        if ( strcmp( inMethod, dbHeader.weakAuthMethods[index].method ) == 0 )
            return 1;
    
    return 0;
}


-(int)addWeakAuthMethod:(const char *)inMethod
{
	int index;
    PWFileHeader ourHeader;
    int result = 0;
   
	if ( [self isWeakAuthMethod:inMethod] )
		return 0;
	 
	[self pwLock:kOneSecondLockInterval];
	
    result = [self getHeader:&ourHeader];
    if ( result == 0 || result == -3 )
    {
		for ( index = 0; index < kPWFileMaxWeakMethods; index++ )
		{
			if ( ourHeader.weakAuthMethods[index].method[0] == 0 )
			{
				strcpy( ourHeader.weakAuthMethods[index].method, inMethod );
				result = [self setHeader:&ourHeader];
				break;
			}
		}
	}
	
	[self pwUnlock];
	
    return result;
}


-(int)removeWeakAuthMethod:(const char *)inMethod
{
	int index;
    PWFileHeader ourHeader;
    int result = 0;
    
    [self pwLock:kOneSecondLockInterval];
	
    result = [self getHeader:&ourHeader];
    if ( result == 0 || result == -3 )
    {
		for ( index = 0; index < kPWFileMaxWeakMethods; index++ )
		{
			if ( strcmp( inMethod, mPWFileHeader.weakAuthMethods[index].method ) == 0 )
			{
				bzero( mPWFileHeader.weakAuthMethods[index].method, SASL_MECHNAMEMAX+1 );
				result = [self setHeader:&ourHeader];
				break;
			}
		}
	}
	
	[self pwUnlock];
	
    return result;
}


//----------------------------------------------------------------------------------------------------
//	expandDatabase
//
//	Returns: file errors
//
//	Expands the database file to allocate room for new slots.
//	If outSlot is NULL, no slots are assigned; otherwise, the next available slot is returned.
//----------------------------------------------------------------------------------------------------

-(int)expandDatabase:(unsigned long)inNumSlots nextAvailableSlot:(long *)outSlot
{
	int err;
	int writeCount;
	
	if ( mReadOnlyFileSystem )
		return -1;
	
	[self pwWait];
	
	err = [self openPasswordFile:"r+"];
	if ( err == 0 && mPWFile != NULL )
	{
		// write blank space
		err = fseek( mPWFile, 0, SEEK_END );
		if ( err == 0 )
		{
			PWFileEntry anEntry;
			int i;
			
			bzero( &anEntry, sizeof(PWFileEntry) );
			
			for ( i = inNumSlots; i > 0; i-- )
			{
				writeCount = fwrite( &anEntry, sizeof(PWFileEntry), 1, mPWFile );
				if ( writeCount != 1 )
				{
					err = -1;
					break;
				}
			}
		}
		
		// update header
		mPWFileHeader.numberOfSlotsCurrentlyInFile += inNumSlots;
		if ( outSlot != NULL )
		{
			mPWFileHeader.deepestSlotUsed++;
			mPWFileHeader.deepestSlotUsedByThisServer = mPWFileHeader.deepestSlotUsed;
			*outSlot = mPWFileHeader.deepestSlotUsed;
		}
		
		err = [self setHeader:&mPWFileHeader];
	}
	
	[self pwSignal];
	
	return err;
}


// ---------------------------------------------------------------------------
//	getBigNumber
// ---------------------------------------------------------------------------

-(BOOL)getBigNumber:(char **)outBigNumStr
{
    BIGNUM *nonce;
	char *bnStr = NULL;
	
    if ( outBigNumStr == NULL )
		return false;
	*outBigNumStr = NULL;
	
	// make nonce
	nonce = BN_new();
	if ( nonce == NULL )
		return false;
	
	// Generate a random challenge (256-bits)
	BN_rand(nonce, 256, 0, 0);
	bnStr = BN_bn2dec(nonce);
	
	BN_clear_free(nonce);
	
	if ( bnStr == NULL )
		return false;
    
	*outBigNumStr = bnStr;
    
    return true;
}


//----------------------------------------------------------------------------------------------------
//	nextSlot
//
//	Returns: 0 for invalid/error, or the next slot number in the pw file for writing the next entry.
//----------------------------------------------------------------------------------------------------

-(long)nextSlot
{
    long slot = 0;
    int err = -1;
    off_t curpos = 0;
    long readCount;
    PWFileEntry dbEntry;
	
    if ( mPWFileValidated )
    {
		#if DEBUG
		if ( mTestSpillBucket )
			return 2;
		#endif
		
		if ( mPWFileHeader.deepestSlotUsedByThisServer < mPWFileHeader.numberOfSlotsCurrentlyInFile - 1 )
		{
			err = [self getPasswordRec:mPWFileHeader.deepestSlotUsedByThisServer + 1 putItHere:&dbEntry];
			if ( err == 0 &&
				 dbEntry.time == 0 &&
				 dbEntry.rnd == 0 &&
				 dbEntry.sequenceNumber == 0 &&
				 dbEntry.slot == 0 )
			{
				mPWFileHeader.deepestSlotUsedByThisServer++;
				slot = mPWFileHeader.deepestSlotUsedByThisServer;
				if ( mPWFileHeader.deepestSlotUsedByThisServer > mPWFileHeader.deepestSlotUsed )
					mPWFileHeader.deepestSlotUsed = mPWFileHeader.deepestSlotUsedByThisServer;
				
				return slot;
			}
		}
		
        if ( mPWFileHeader.deepestSlotUsed < mPWFileHeader.numberOfSlotsCurrentlyInFile - 1 )
        {
            mPWFileHeader.deepestSlotUsed++;
			mPWFileHeader.deepestSlotUsedByThisServer = mPWFileHeader.deepestSlotUsed;
            slot = mPWFileHeader.deepestSlotUsed;
        }
        else
        {
            // go look in the freelist
            mFreeListFile = fopen( kFreeListFilePath, "r+" );
            if ( mFreeListFile != NULL )
            {
				[self pwWait];
                do
				{
					err = fseek( mFreeListFile, -sizeof(long), SEEK_END );
					if ( err == 0 )
					{
						curpos = ftell( mFreeListFile );
						readCount = fread( &slot, sizeof(long), 1, mFreeListFile );
						if ( readCount == 1 )
						{
							// snip the one we used
							err = ftruncate( fileno(mFreeListFile), curpos );
							if ( err == 0 )
							{
								// double-check that the slot is really free
								err = [self getPasswordRec:slot putItHere:&dbEntry unObfuscate:NO];
								if ( err == 0 && !PWRecIsZero(dbEntry) )
									err = -1;
							}
						}
						else
						{
							err = -1;
							break;
						}
					}
				}
				while ( err == -1 && curpos > 0 );
				[self closeFreeListFile];
				[self pwSignal];
            }
			
            // if freelist is empty, expand the file
            if ( err != 0 || slot == 0 )
                err = [self expandDatabase:kPWFileInitialSlots nextAvailableSlot:&slot];
        }
    }
    
    return slot;
}


//----------------------------------------------------------------------------------------------------
//	addRSAKeys 
//
//	Returns: 0 or -1
//	Adds RSA version 2 keys to the password database header using the ssh-keygen tool
//----------------------------------------------------------------------------------------------------

-(int)addRSAKeys
{
	return [self addRSAKeys:1024];
}


//----------------------------------------------------------------------------------------------------
//	addRSAKeys 
//
//	Returns: 0 or -1
//	Adds RSA version 2 keys to the password database header using the ssh-keygen tool
//----------------------------------------------------------------------------------------------------

-(int)addRSAKeys:(unsigned int)inBitCount
{
	FILE *aFile = NULL;
    int result = -1;
	unsigned char *publicKey = NULL;
	unsigned long publicKeyLen = 0;
	unsigned char *privateKey = NULL;
	unsigned long privateKeyLen = 0;
	char bitCountStr[256] = {0,};
    char tempFileStr[256] = {0,};
    char publicKeyFileStr[256] = {0,};
    struct stat sb = {0};
	char *argv[] = {	"/usr/bin/ssh-keygen",
						"-t", "rsa1",
						"-b", bitCountStr,
						"-f", tempFileStr,
						"-P", "",
						NULL };
	
	// setup command parameters
	sprintf( bitCountStr, "%u", inBitCount );
	strcpy( tempFileStr, kTempKeyTemplate );
	if ( mktemp(tempFileStr) == NULL )
		return -1;
	
	do
	{
		// make the keys
		if ( pwsf_LaunchTask("/usr/bin/ssh-keygen", argv) != EX_OK )
			break;
		
		// stat the key file, get the length
		if ( lstat(tempFileStr, &sb) != 0 )
			break;
		
		if ( !S_ISREG(sb.st_mode) || sb.st_nlink != 1 )
			break;
		
		// add the private key
		aFile = fopen( tempFileStr, "r" );
		if ( aFile != NULL )
		{
			privateKeyLen = (unsigned long)sb.st_size;
			privateKey = (unsigned char *) malloc( privateKeyLen + 1 );
			fread( (char*)privateKey, (unsigned long)sb.st_size, 1, aFile );
			fclose( aFile );
			
			// stat the public key file, get the length
			sprintf( publicKeyFileStr, "%s.pub", tempFileStr );
			if ( lstat(publicKeyFileStr, &sb) != 0 )
				break;
			
			// add the public key
			aFile = fopen( publicKeyFileStr, "r" );
			if ( aFile != NULL )
			{
				publicKeyLen = (unsigned long)sb.st_size;
				publicKey = (unsigned char *) malloc( publicKeyLen + 1 );
				fread( publicKey, (unsigned long)sb.st_size, 1, aFile );
				fclose( aFile );
				
				result = [self addRSAKeys:publicKey publicKeyLen:publicKeyLen privateKey:privateKey privateKeyLen:privateKeyLen];
			}
		}
	}
	while ( 0 );
	
	// we are done with these
	if ( publicKeyFileStr[0] != '\0' )
		unlink( publicKeyFileStr );
	if ( tempFileStr[0] != '\0' )
		unlink( tempFileStr );
	if ( privateKey != NULL ) {
		bzero( privateKey, privateKeyLen );
		free( privateKey );
	}
	if ( publicKey != NULL )
		free( publicKey );
	
    return result;
}


//----------------------------------------------------------------------------------------------------
//	addRSAKeys 
//
//	Returns: 0 or -1
//	Adds RSA version 2 keys to the password database header using the ssh-keygen tool
//----------------------------------------------------------------------------------------------------

-(int)addRSAKeys:(unsigned char *)publicKey
	publicKeyLen:(unsigned long)publicKeyLen
	privateKey:(unsigned char *)privateKey
	privateKeyLen:(unsigned long)privateKeyLen
{
    PWFileHeader ourHeader;
    int result;
	
    if ( privateKeyLen > kPWFileMaxPrivateKeyBytes )
        return -1;
    
    if ( publicKeyLen > kPWFileMaxPublicKeyBytes )
        return -1;
	
    // retrieve the pw database header
    result = [self getHeader:&ourHeader];
    if ( result != 0 && result != -3 )
        return result;
    
    ourHeader.privateKeyLen = privateKeyLen;
	memcpy( ourHeader.privateKey, privateKey, privateKeyLen );
    
    ourHeader.publicKeyLen = publicKeyLen;
	memcpy( ourHeader.publicKey, publicKey, publicKeyLen );
    
    // write it back to the pw database file
	result = [self setHeader:&ourHeader];
    
    // do not leave the private key sitting around in the stack
    bzero(&ourHeader, sizeof(ourHeader));
    
    return result;
}


//----------------------------------------------------------------------------------------------------
//	addGenesisUser 
//
//	Returns: errno
//	Creates an initial Admin user in slot 1 so that the database can be edited.
//	This operation should not be done by the password server. If an existing
//	password file were moved or damaged, it could give a hacker free reign.
//	This method should only be called by a tool on the local CPU that is only run by root.
//	(Setup Assistant, for example).
//----------------------------------------------------------------------------------------------------

-(int)addGenesisUser:(const char *)username password:(const char *)password pwsRec:(PWFileEntry *)outPWRec
{
	PWFileHeader dbHeader;
    PWFileEntry passwordRec;
    int err;
	int err2 = 0;
	
    bzero(&passwordRec, sizeof(passwordRec));
    
    passwordRec.time = 0;
    passwordRec.rnd = 0;
    passwordRec.sequenceNumber = 0;
    passwordRec.slot = 1;
    
    passwordRec.access.isDisabled = false;
    passwordRec.access.isAdminUser = true;
    passwordRec.access.newPasswordRequired = false;
    passwordRec.access.usingHistory = false;
    passwordRec.access.canModifyPasswordforSelf = true;
    passwordRec.access.usingExpirationDate = false;
    passwordRec.access.usingHardExpirationDate = false;
    passwordRec.access.requiresAlpha = false;
    passwordRec.access.requiresNumeric = false;
    passwordRec.access.passwordIsHash = false;
	
    passwordRec.access.maxMinutesOfNonUse = 0;
    passwordRec.access.maxFailedLoginAttempts = 0;
    passwordRec.access.minChars = 0;
    passwordRec.access.maxChars = 0;
    
    strcpy( passwordRec.usernameStr, (username) ? username : "admin" );
    strcpy( passwordRec.passwordStr, (password) ? password : "admin" );
    
	[self pwLock:kOneSecondLockInterval];
	
	err = [self getHeader:&dbHeader];
	if ( err == 0 || err == -3 )
	{
		// mark the slot used if the database is new
		// for established databases, we're just replacing the system administrator
		if ( dbHeader.sequenceNumber == 0 && dbHeader.deepestSlotUsed == 0 )
		{
			dbHeader.sequenceNumber++;
			dbHeader.deepestSlotUsed++;
			dbHeader.deepestSlotUsedByThisServer++;
		}
		
		err = [self setPassword:&passwordRec atSlot:passwordRec.slot];
		if ( err == 0 && outPWRec != NULL )
		{
			memcpy( outPWRec, &passwordRec, sizeof(PWFileEntry) );
		}
		
		err2 = [self setHeader:&dbHeader];
	}
	
	[self pwUnlock];
	
	if ( err == 0 && err2 != 0 )
		err = err2;
	
	return err;
}


//----------------------------------------------------------------------------------------------------
//	addPassword 
//
//	Returns: errno
//	Used to add new passwords
//----------------------------------------------------------------------------------------------------

-(int)addPassword:(PWFileEntry *)passwordRec obfuscate:(BOOL)obfuscate
{
    PWFileHeader ignoreHeader;
    int err, err2;
	
	[self pwLock:kOneSecondLockInterval];
	
    // refresh the header
	// the retrieved header is ignored because the nextSlot() method uses
	// the object's copy of the header in mPWFileHeader.
    err = [self getHeader:&ignoreHeader];
    if ( err != 0 && err != -3 )
		return err;
	
    passwordRec->time = pwsf_getTimeForRef();
    passwordRec->rnd = pwsf_getRandom();
    passwordRec->sequenceNumber = ++mPWFileHeader.sequenceNumber;
    passwordRec->slot = [self nextSlot];
    
    pwsf_getGMTime( (struct tm *)&passwordRec->creationDate );
    memcpy( &passwordRec->lastLogin, &passwordRec->creationDate, sizeof(struct tm) );
    memcpy( &passwordRec->modDateOfPassword, &passwordRec->creationDate, sizeof(struct tm) );
    
    err = [self setPassword:passwordRec atSlot:passwordRec->slot obfuscate:obfuscate setModDate:YES];
	
	// re-write the header to mark the slot used.
	err2 = [self setHeader:&mPWFileHeader];
	
	[self pwUnlock];
	
	if ( err == 0 && err2 != 0 )
		err = err2;
	
	return err;
}


//----------------------------------------------------------------------------------------------------
//	addPassword:atSlot
//
//	Returns: errno
//	Used to add password records from replicas. Fills in the slot if free; otherwise redirects the
//	record to the spill-bucket.
//----------------------------------------------------------------------------------------------------

-(int)addPassword:(PWFileEntry *)passwordRec atSlot:(long)slot obfuscate:(BOOL)obfuscate
{
	return [self addPassword:passwordRec atSlot:slot obfuscate:obfuscate setModDate:YES];
}

-(int)addPassword:(PWFileEntry *)passwordRec atSlot:(long)slot obfuscate:(BOOL)obfuscate setModDate:(BOOL)setModDate
{
	PWFileEntry dbEntry;
	int err;
	bool bGoesInMainDB = false;
	
	// verifying the slot id, do not need to un-obfuscate
	err = [self getPasswordRec:passwordRec->slot putItHere:&dbEntry unObfuscate:NO];
	if ( err != 0 )
		return err;
	
	if ( passwordRec->time == dbEntry.time &&
			passwordRec->rnd == dbEntry.rnd &&
			passwordRec->sequenceNumber == dbEntry.sequenceNumber &&
			passwordRec->slot == dbEntry.slot )
	{
		// same user
		bGoesInMainDB = YES;
	}
	else
	if ( dbEntry.time == 0 && dbEntry.rnd == 0 &&
			dbEntry.sequenceNumber == 0 && dbEntry.slot == 0 )
	{
		// slot free
		bGoesInMainDB = YES;
	}
	
	if ( bGoesInMainDB )
	{
		err = [self setPassword:passwordRec atSlot:passwordRec->slot obfuscate:obfuscate setModDate:setModDate];
	}
	else
	{
		[mOverflow saveOverflowRecord:passwordRec obfuscate:obfuscate setModDate:setModDate];
	}
	
	return err;
}


//----------------------------------------------------------------------------------------------------
//	addPasswordAtSlotFast
//
//	Returns: errno
//
//	WARNING: passwordRec is invalid on exit.
//	Used to add password records from replicas. Fills in the slot if free; otherwise redirects the
//	record to the spill-bucket. Similar to the original addPasswordAtSlot() method, but:
//	obfuscate is YES, but the password is not un-obfuscated.
//	setModDate is YES
//----------------------------------------------------------------------------------------------------

-(int)addPasswordFast:(PWFileEntry *)passwordRec atSlot:(unsigned long)slot
{
	PWFileEntry dbEntry;
	int err;
	bool bGoesInMainDB = false;

	// verifying the slot id, do not need to un-obfuscate
	err = [self getPasswordRec:passwordRec->slot putItHere:&dbEntry unObfuscate:NO];
	if ( err != 0 )
		return err;

	if ( passwordRec->time == dbEntry.time &&
			passwordRec->rnd == dbEntry.rnd &&
			passwordRec->sequenceNumber == dbEntry.sequenceNumber &&
			passwordRec->slot == dbEntry.slot )
	{
		// same user
		bGoesInMainDB = YES;
	}
	else
	if ( dbEntry.time == 0 && dbEntry.rnd == 0 &&
			dbEntry.sequenceNumber == 0 && dbEntry.slot == 0 )
	{
		// slot free
		bGoesInMainDB = YES;
	}

	if ( bGoesInMainDB )
	{
		err = [self setPasswordFast:passwordRec atSlot:passwordRec->slot];
	}
	else
	{
		[mOverflow saveOverflowRecord:passwordRec obfuscate:YES setModDate:YES];
	}

	return err;
}


//----------------------------------------------------------------------------------------------------
//	initPasswordRecord
//
//	Returns: errno
//	Used to add new passwords
//	Almost identical to addPassword() but doesn't write out the password record.
//	By default, use the addPassword() method. This method is used for optimizations.
//----------------------------------------------------------------------------------------------------

-(int)initPasswordRecord:(PWFileEntry *)passwordRec obfuscate:(BOOL)obfuscate
{
	PWFileHeader ignoreHeader;
	int err, err2;

	[self pwLock:kOneSecondLockInterval];
	
	// refresh the header
	// the retrieved header is ignored because the nextSlot() method uses
	// the object's copy of the header in mPWFileHeader.
	err = [self getHeader:&ignoreHeader];
	if ( err != 0 && err != -3 )
		return err;

	passwordRec->time = pwsf_getTimeForRef();
	passwordRec->rnd = pwsf_getRandom();
	passwordRec->sequenceNumber = ++mPWFileHeader.sequenceNumber;
	passwordRec->slot = [self nextSlot];

	pwsf_getGMTime( (struct tm *)&passwordRec->creationDate );
	memcpy( &passwordRec->lastLogin, &passwordRec->creationDate, sizeof(struct tm) );
	memcpy( &passwordRec->modDateOfPassword, &passwordRec->creationDate, sizeof(struct tm) );

	// re-write the header to mark the slot used.
	err2 = [self setHeader:&mPWFileHeader];

	[self pwUnlock];
	
	if ( err == 0 && err2 != 0 )
		err = err2;

	return err;
}


//------------------------------------------------------------------------------------------------
//	newPasswordForUser
//
//	Returns: errno
//	Similar to AddPassword() but faster for batch operations. 
//	Calls initPasswordRecord() which does not save the password record.
//	By default, use the AddPassword() method.
//------------------------------------------------------------------------------------------------

-(int)newPasswordForUser:(const char *)inUser
		password:(const char *)inPassword
		slotStr:(char *)outPasswordRef
		slotRec:(PWFileEntry *)inOutUserRec
{
	int result;

	if ( inUser == NULL || inPassword == NULL || outPasswordRef == NULL || inOutUserRec == NULL )
		return -1;

	if ( strlen(inPassword) > 511 )
		return kAuthPasswordTooLong;

	bzero( inOutUserRec, sizeof(PWFileEntry) );
	inOutUserRec->access.canModifyPasswordforSelf = true;

	strcpy( inOutUserRec->usernameStr, inUser );
	strcpy( inOutUserRec->passwordStr, inPassword );

	result = [self initPasswordRecord:inOutUserRec obfuscate:YES];

	pwsf_passwordRecRefToString( inOutUserRec, outPasswordRef );

	return result;
}


-(int)getPasswordRec:(long)slot putItHere:(PWFileEntry *)passRec
{
	return [self getPasswordRec:slot putItHere:passRec unObfuscate:YES];
}

-(int)getPasswordRec:(long)slot putItHere:(PWFileEntry *)passRec unObfuscate:(BOOL)unObfuscate
{
    long offset;
    int err = -1;
    ssize_t readCount;
    
    if ( slot > 0 )
    {
        [self pwWait];
        err = [self openPasswordFile:mReadOnlyFileSystem ? "r" : "r+"];
        if ( err == 0 && mPWFile )
        {
            offset = pwsf_slotToOffset( slot );
            
			readCount = pread( mPWFileNO, passRec, sizeof(PWFileEntry), offset );
			if ( readCount != sizeof(PWFileEntry) )
			{
				// failure could indicate a problem with the file descriptor
				// get a new one next time
				[self closePasswordFile];
				
				err = -2;
			}
            else
			{
				pwsf_EndianAdjustPWFileEntry( passRec, 1 );
			}
			
            // recover the password
			if ( unObfuscate && !PWRecIsZero(*passRec) )
				pwsf_DESAutoDecode( passRec->passwordStr );
        }
        [self pwSignal];
    }
    
    return err;
}


//----------------------------------------------------------------------------------------------------
//	getValidPasswordRec
//
//	Returns: errno
//	same as getPasswordRec but validates the record's ref numbers.
//----------------------------------------------------------------------------------------------------

-(int)getValidPasswordRec:(PWFileEntry *)passwordRec
{
	return [self getValidPasswordRec:passwordRec fromSpillBucket:NULL unObfuscate:YES];
}

-(int)getValidPasswordRec:(PWFileEntry *)passwordRec fromSpillBucket:(BOOL *)outFromSpillBucket		// default NULL
	unObfuscate:(BOOL)unObfuscate
{
    int err;
    PWFileEntry dbEntry;
    
	if ( outFromSpillBucket != NULL )
		*outFromSpillBucket = false;
	
    err = [self getPasswordRec:passwordRec->slot putItHere:&dbEntry unObfuscate:unObfuscate];
    if ( err != 0 )
		return err;
	
	if ( passwordRec->time == dbEntry.time &&
			passwordRec->rnd == dbEntry.rnd &&
			passwordRec->sequenceNumber == dbEntry.sequenceNumber &&
			passwordRec->slot == dbEntry.slot )
	{
		memcpy( passwordRec, &dbEntry, sizeof(PWFileEntry) );
	}
	else
	{
		err = [mOverflow getPasswordRecFromSpillBucket:passwordRec unObfuscate:unObfuscate];
		if ( err == 0 )
		{
			if ( outFromSpillBucket != NULL )
				*outFromSpillBucket = YES;
		}
	}
    
    return err;
}


//----------------------------------------------------------------------------------------------------
//	getValidOrZeroPasswordRec
//
//	Returns: errno
//	same as getValidPasswordRec but returns the empty slot and err=0 if the main DB slot is empty.
//----------------------------------------------------------------------------------------------------

-(int)getValidOrZeroPasswordRec:(PWFileEntry *)passwordRec fromSpillBucket:(BOOL *)outFromSpillBucket		// default NULL
	unObfuscate:(BOOL)unObfuscate
{
    int err;
    PWFileEntry dbEntry;
    
	if ( outFromSpillBucket != NULL )
		*outFromSpillBucket = false;
	
    err = [self getPasswordRec:passwordRec->slot putItHere:&dbEntry unObfuscate:unObfuscate];
    if ( err != 0 )
		return err;
	
	if ( passwordRec->time == dbEntry.time &&
			passwordRec->rnd == dbEntry.rnd &&
			passwordRec->sequenceNumber == dbEntry.sequenceNumber &&
			passwordRec->slot == dbEntry.slot )
	{
		memcpy( passwordRec, &dbEntry, sizeof(PWFileEntry) );
	}
	else
	{
		err = [mOverflow getPasswordRecFromSpillBucket:passwordRec unObfuscate:unObfuscate];
		if ( err == 0 )
		{
			if ( outFromSpillBucket != NULL )
				*outFromSpillBucket = YES;
		}
		else
		{
			if ( PWRecIsZero(dbEntry) )
			{
				memcpy( passwordRec, &dbEntry, sizeof(PWFileEntry) );
				err = 0;
			}
		}
	}
    
    return err;
}


//------------------------------------------------------------------------------------------------
//	purgeDeadSlots
//
//	RETURNS: an array of purged slot IDs
//
//	deleteWaitSeconds
//	The amount of time to wait after the deletion has been sent to all replicas
//
//	purgeWaitSeconds
//	The amount of time beyond which records are purged no matter what
//------------------------------------------------------------------------------------------------

-(CFArrayRef)purgeDeadSlots:(CFDateRef)beforeDate deleteWait:(long)deleteWaitSeconds purgeWait:(long)purgeWaitSeconds
{
	int err = 0;
	CFMutableArrayRef purgedSlotArray = NULL;
	CFStringRef purgedSlotString = NULL;
	UInt32 index = 0;
	struct tm beforeTime;
	time_t beforeSecs;
	time_t beforePurgeSecs;
	time_t deleteSecs;
	PWFileHeader dbHeader;
	PWFileEntry passRec;
	char idStr[35];
	
	if ( beforeDate == NULL )
		return NULL;
	
	purgedSlotArray = CFArrayCreateMutable( kCFAllocatorDefault, 0, &kCFTypeArrayCallBacks );
	if ( purgedSlotArray == NULL )
		return NULL;
	
	err = [self getHeader:&dbHeader];
	if ( err != 0 && err != -3 ) {
		CFRelease( purgedSlotArray );
		return NULL;
	}
	
	CFRetain(beforeDate);
	pwsf_ConvertCFDateToBSDTime((CFDateRef)beforeDate, &beforeTime);
	beforeSecs = timegm( &beforeTime );
	beforePurgeSecs = beforeSecs; 
	
	// subtract off enough time for post-processing of the sync files
	beforeSecs -= deleteWaitSeconds;
	beforePurgeSecs -= purgeWaitSeconds;
	
	for ( index = dbHeader.deepestSlotUsed; index > 0; index-- )
	{
		// Get obfuscated record.
		err = [self getPasswordRec:index putItHere:&passRec unObfuscate:NO];
		if ( err == 0 && !PWRecIsZero(passRec) && passRec.extraAccess.recordIsDead )
		{
			deleteSecs = timegm( (struct tm *)&passRec.modDateOfPassword );
			if ( difftime(deleteSecs, beforeSecs) < 0 || difftime(deleteSecs, beforePurgeSecs) < 0 )
			{
				if ( [self freeSlot:&passRec deathCertificate:NO] == 0 )
				{
					pwsf_passwordRecRefToString( &passRec, idStr );
					purgedSlotString = CFStringCreateWithCString( kCFAllocatorDefault, idStr, kCFStringEncodingUTF8 );
					if ( purgedSlotString != NULL ) {
						CFArrayAppendValue( purgedSlotArray, purgedSlotString );
						CFRelease( purgedSlotString );
						purgedSlotString = NULL;
					}
				}
			}
		}
	}
	
	[mOverflow doActionForAllOverflowFiles:kOverflowActionPurgeDeadSlots
		principal:NULL userRec:NULL purgeBefore:beforeSecs];
	
	CFRelease(beforeDate);
	
	return purgedSlotArray;
}


-(int)freeSlot:(PWFileEntry *)passwordRec
{
	[self setShouldSyncNow:YES];
	return [self freeSlot:passwordRec deathCertificate:YES];
}


-(int)freeSlot:(PWFileEntry *)passwordRec deathCertificate:(BOOL)useDeathCertificate
{
    int err;
    long slot = passwordRec->slot;
    long writeCount;
    PWFileEntry deleteRec;
	BOOL fromSpillBucket;
	
	// start with a zero record
	bzero( &deleteRec, sizeof(PWFileEntry) );
	
	// keep slot ID for routing in the DB
	deleteRec.time = passwordRec->time;
	deleteRec.rnd = passwordRec->rnd;
	deleteRec.sequenceNumber = passwordRec->sequenceNumber;
	deleteRec.slot = passwordRec->slot;
	
	if ( useDeathCertificate )
	{
		// keep the ID, mark the time of deletion in modDateOfPassword,
		// and mark dead.
		pwsf_getGMTime( (struct tm *)&deleteRec.modDateOfPassword );
		deleteRec.extraAccess.recordIsDead = true;
		deleteRec.changeTransactionID = passwordRec->changeTransactionID;
		
		// we need the kerberos <principal@@realm> information
		// to replicate the deletion
		memcpy( &deleteRec.digest[kPWHashSlotKERBEROS],
				&passwordRec->digest[kPWHashSlotKERBEROS],
				sizeof(PasswordDigest) );
		
		memcpy( &deleteRec.digest[kPWHashSlotKERBEROS_NAME],
				&passwordRec->digest[kPWHashSlotKERBEROS_NAME],
				sizeof(PasswordDigest) );
	}
	
    // original rec must be valid to have permission to clear a slot
    err = [self getValidPasswordRec:passwordRec fromSpillBucket:&fromSpillBucket unObfuscate:NO];
    if ( err == 0 )
    {		
		if ( fromSpillBucket )
		{
			if ( useDeathCertificate )
				err = [mOverflow saveOverflowRecord:&deleteRec obfuscate:YES setModDate:YES];
			else
				err = [mOverflow deleteSlot:&deleteRec];
		}
		else
		{
			err = [self setPassword:&deleteRec atSlot:slot];
			
			if ( ! useDeathCertificate )
			{
				// add the slot number to free list
				[self pwWait];
				mFreeListFile = fopen( kFreeListFilePath, "a+" );
				if ( mFreeListFile != NULL )
				{
					writeCount = fwrite( &slot, sizeof(long), 1, mFreeListFile );
					if ( writeCount != 1 )
					{
						// may have a forgotten slot
						err = -1;
					}
					
					[self closeFreeListFile];
				}
				[self pwSignal];
			}
		}
    }
	else
	{
		// no record, but invalidate the ID just to be sure
		if ( useDeathCertificate )
			[self addPassword:&deleteRec atSlot:slot obfuscate:NO];
	}
	
    return err;
}


-(int)setPassword:(PWFileEntry *)passwordRec atSlot:(unsigned long)slot
{
	return [self setPassword:passwordRec atSlot:slot obfuscate:YES setModDate:YES];
}

-(int)setPassword:(PWFileEntry *)passwordRec
	atSlot:(unsigned long)slot
	obfuscate:(BOOL)obfuscate
	setModDate:(BOOL)setModDate
{
    long offset;
    int err = -1;
    int writeCount;
    unsigned int encodeLen;
	
	if ( mReadOnlyFileSystem )
		return -1;
	
    if ( slot > 0 )
    {
		if ( (unsigned long)slot > mPWFileHeader.numberOfSlotsCurrentlyInFile )
			return -1;
		
		if ( setModDate )
			pwsf_getGMTime( (struct tm *)&passwordRec->modificationDate );
        
        [self pwWait];
        err = [self openPasswordFile:"r+"];
        if ( err == 0 && mPWFile != NULL )
        {
            offset = pwsf_slotToOffset( slot );
            
            err = fseek( mPWFile, offset, SEEK_SET );
            if ( err == 0 )
            {
                //passwordRec->slot = slot;
                encodeLen = strlen(passwordRec->passwordStr);
                encodeLen += (kFixedDESChunk - (encodeLen % kFixedDESChunk));	
                if ( encodeLen > sizeof(passwordRec->passwordStr) )
                    encodeLen = sizeof(passwordRec->passwordStr);
                
				if ( obfuscate )
					pwsf_DESEncode(passwordRec->passwordStr, encodeLen);
				
				pwsf_EndianAdjustPWFileEntry( passwordRec, 0 );
                writeCount = fwrite( passwordRec, sizeof(PWFileEntry), 1, mPWFile );
				pwsf_EndianAdjustPWFileEntry( passwordRec, 1 );
				
                if ( obfuscate )
					pwsf_DESDecode(passwordRec->passwordStr, encodeLen);
				
				if ( writeCount == 1 )
					fflush( mPWFile );
                else
                    err = -1;
            }
		}
        [self pwSignal];
    }

    return err;
}


//----------------------------------------------------------------------------------------------------
//	setPasswordAtSlotFast
//
//	Returns: errno
//
//	WARNING: passwordRec is invalid on exit.
//	Used to write to a specific slot. Similar to the original setPasswordAtSlot() method, but:
//	setModeDate is TRUE
//	obfuscate is TRUE, but the password is not un-obfuscated.
//----------------------------------------------------------------------------------------------------

-(int)setPasswordFast:(PWFileEntry *)passwordRec atSlot:(unsigned long)slot
{
	long offset;
	int err = -1;
	int writeCount;
	unsigned int encodeLen;

	if ( mReadOnlyFileSystem )
		return -1;

	if ( slot > 0 )
	{
		if ( (unsigned long)slot > mPWFileHeader.numberOfSlotsCurrentlyInFile )
			return -1;
		
		pwsf_getGMTime( (struct tm *)&passwordRec->modificationDate );
		
		[self pwWait];
		err = [self openPasswordFile:"r+"];
		if ( err == 0 && mPWFile != NULL )
		{
			offset = pwsf_slotToOffset( slot );
			
			err = fseek( mPWFile, offset, SEEK_SET );
			if ( err == 0 )
			{
				//passwordRec->slot = slot;
				encodeLen = strlen(passwordRec->passwordStr);
				encodeLen += (kFixedDESChunk - (encodeLen % kFixedDESChunk));	
				if ( encodeLen > sizeof(passwordRec->passwordStr) )
					encodeLen = sizeof(passwordRec->passwordStr);
								
				pwsf_DESEncode(passwordRec->passwordStr, encodeLen);
				
				pwsf_EndianAdjustPWFileEntry( passwordRec, 0 );
                writeCount = fwrite( passwordRec, sizeof(PWFileEntry), 1, mPWFile );
				pwsf_EndianAdjustPWFileEntry( passwordRec, 1 );
				
				if ( writeCount == 1 )
					fflush( mPWFile );
				else
					err = -1;
			}
		}
		[self pwSignal];
	}

	return err;
}


//------------------------------------------------------------------------------------------------
//	addHashes
//
//	inRealm				 ->		the realm to use for the DIGEST-MD5 hash
//	inOutPasswordRec	<->		in clear-text, out hash values
//	Takes the clear-text password and adds the hashes for auth methods
//------------------------------------------------------------------------------------------------

-(void)addHashes:(const char *)inRealm addNT:(BOOL)inAddNT addLM:(BOOL)inAddLM pwsRec:(PWFileEntry *)inOutPasswordRec
{
	pwsf_AddHashesToPWRecord( inRealm, (bool)inAddNT, (bool)inAddLM, inOutPasswordRec );
}


#pragma mark -
#pragma mark ADDITIONAL DATA FILE
#pragma mark -

//------------------------------------------------------------------------------------------------
//	addGroup:toAdmin:
//
//	Returns: YES if the group is added or present, NO if there is an error or the state is unknown.
//
//	The plist file of groups is saved to disk by this method; the slot record is not. It is the
//	caller's responsibility to write the slot.
//------------------------------------------------------------------------------------------------

-(BOOL)addGroup:(uuid_t)groupUUID toAdmin:(PWFileEntry *)inOutAdminRec
{
	BOOL result = NO;
	CFMutableDictionaryRef groupDict = NULL;
	CFMutableArrayRef groupArray = NULL;
	CFStringRef uuidString = NULL;
	char filePath[PATH_MAX] = {0};
	int err;
	uuid_t groupList[2];
	
	if (inOutAdminRec == NULL || inOutAdminRec->access.isAdminUser == 0)
		return NO;
	
	switch( inOutAdminRec->admingroup.list_type )
	{
		case kPWGroupNotSet:
			inOutAdminRec->admingroup.list_type = kPWGroupInSlot;
			memcpy( inOutAdminRec->admingroup.group_uuid, groupUUID, sizeof(uuid_t) );
			result = YES;
			break;
		
		case kPWGroupInSlot:
			if ( memcmp(&(inOutAdminRec->admingroup.group_uuid), groupUUID, sizeof(uuid_t)) != 0 )
			{
				memcpy( &groupList[0], &(inOutAdminRec->admingroup.group_uuid), sizeof(uuid_t) );
				memcpy( &groupList[1], (uuid_t *)groupUUID, sizeof(uuid_t) );
				groupDict = pwsf_CreateAdditionalDataDictionaryWithUUIDList( 2, (uuid_t *)groupList );
				if ( groupDict == NULL )
					break;
				
				[self setFilePath:filePath maxPathSize:sizeof(filePath) forEntry:inOutAdminRec];
				if ( pwsf_savexml(filePath, groupDict) == 0 )
				{
					// set record data
					inOutAdminRec->admingroup.list_type = kPWGroupInFile;
					bzero( &inOutAdminRec->admingroup.group_uuid, sizeof(uuid_t) );
					result = YES;
				}
			}
			break;
		
		case kPWGroupInFile:
			[self setFilePath:filePath maxPathSize:sizeof(filePath) forEntry:inOutAdminRec];
			err = pwsf_loadxml( filePath, &groupDict );
			if ( err == 0 )
			{
				groupArray = (CFMutableArrayRef) CFDictionaryGetValue( groupDict, CFSTR(kPWKey_ScopeOfAuthority) );
				if ( groupArray == NULL )
					break;
	
				uuidString = pwsf_UUIDToString( groupUUID );
				if ( uuidString == NULL )
					break;
				
				if ( !CFArrayContainsValue(groupArray, CFRangeMake(0, CFArrayGetCount(groupArray)), uuidString) )
					CFArrayAppendValue( groupArray, uuidString );
				
				// It's important to set groupArray to NULL.
				// The memory belongs to groupDict which is also released.
				groupArray = NULL;
				
				pwsf_savexml( filePath, groupDict );
			}
			else
			{
				groupDict = pwsf_CreateAdditionalDataDictionaryWithUUIDList( 1, (uuid_t *)&groupUUID );
			}
			break;
	}
	
	// clean up
	if ( groupDict != NULL )
		CFRelease( groupDict );
	if ( groupArray != NULL )
		CFRelease( groupArray );
	if ( uuidString != NULL )
		CFRelease( uuidString );
	
	return result;
}


//------------------------------------------------------------------------------------------------
//	removeGroup:fromAdmin:
//
//	Returns: YES if the group is removed or not present, NO if there was an error and the state
//			 is unknown.
//
//	The plist file of groups is saved to disk by this method; the slot record is not. It is the
//	caller's responsibility to write the slot.
//------------------------------------------------------------------------------------------------

-(BOOL)removeGroup:(uuid_t)groupUUID fromAdmin:(PWFileEntry *)inOutAdminRec
{
	BOOL result = NO;
	CFMutableDictionaryRef groupDict = NULL;
	CFMutableArrayRef groupArray = NULL;
	CFStringRef targetUUIDString = NULL;
	CFStringRef curUUIDString = NULL;
	char filePath[PATH_MAX] = {0};
	CFIndex idx = 0;
	CFIndex groupCount = 0;
	int err = 0;
	
	if (inOutAdminRec == NULL)
		return NO;
	
	switch( inOutAdminRec->admingroup.list_type )
	{
		case kPWGroupNotSet:
			result = YES;
			break;
		
		case kPWGroupInSlot:
			targetUUIDString = pwsf_UUIDToString( groupUUID );
			if ( targetUUIDString == NULL )
				break;
			
			curUUIDString = pwsf_UUIDToString( inOutAdminRec->admingroup.group_uuid );
			if ( curUUIDString == NULL )
				break;
			
			if ( CFStringCompare(targetUUIDString, curUUIDString, 0) == kCFCompareEqualTo )
			{
				inOutAdminRec->admingroup.list_type = kPWGroupNotSet;
				bzero( &inOutAdminRec->admingroup.group_uuid, sizeof(uuid_t) );
				result = YES;
			}
			break;
		
		case kPWGroupInFile:
			[self setFilePath:filePath maxPathSize:sizeof(filePath) forEntry:inOutAdminRec];
			err = pwsf_loadxml( filePath, &groupDict );
			if ( err == 0 )
			{
				groupArray = (CFMutableArrayRef) CFDictionaryGetValue( groupDict, CFSTR(kPWKey_ScopeOfAuthority) );
				if ( groupArray == NULL )
					break;
				
				targetUUIDString = pwsf_UUIDToString( groupUUID );
				if ( targetUUIDString == NULL )
					break;
				
				groupCount = CFArrayGetCount( groupArray );
				for ( idx = 0; idx < groupCount; idx++ )
				{
					curUUIDString = (CFStringRef) CFArrayGetValueAtIndex( groupArray, idx );
					if ( curUUIDString != NULL && CFStringCompare(targetUUIDString, curUUIDString, 0) == kCFCompareEqualTo )
					{
						CFArrayRemoveValueAtIndex( groupArray, idx );
						if ( pwsf_savexml(filePath, groupDict) == 0 )
							result = YES;
						break;
					}
				}
			}
			break;
	}
	
	// clean up
	if ( groupDict != NULL )
		CFRelease( groupDict );
	if ( targetUUIDString != NULL )
		CFRelease( targetUUIDString );
	
	return result;
}


//------------------------------------------------------------------------------------------------
//	removeAllGroupsFromAdmin
//
//	Returns: YES if all groups are removed, NO if there was an error and the state is unknown.
//
//	The plist file of groups is saved to disk by this method; the slot record is not. It is the
//	caller's responsibility to write the slot.
//------------------------------------------------------------------------------------------------

-(BOOL)removeAllGroupsFromAdmin:(PWFileEntry *)inOutAdminRec
{
	BOOL result = NO;
	char filePath[PATH_MAX] = {0};
	
	if (inOutAdminRec == NULL)
		return NO;
	
	switch( inOutAdminRec->admingroup.list_type )
	{
		case kPWGroupNotSet:
			result = YES;
			break;
		
		case kPWGroupInSlot:
			inOutAdminRec->admingroup.list_type = kPWGroupNotSet;
			bzero( &inOutAdminRec->admingroup.group_uuid, sizeof(uuid_t) );
			result = YES;
			break;
		
		case kPWGroupInFile:
			[self setFilePath:filePath maxPathSize:sizeof(filePath) forEntry:inOutAdminRec];
			[self removeKey:CFSTR(kPWKey_ScopeOfAuthority) fromAdditionalDataFile:filePath];
			inOutAdminRec->admingroup.list_type = kPWGroupNotSet;
			bzero( &inOutAdminRec->admingroup.group_uuid, sizeof(uuid_t) );
			result = YES;
			break;
	}
	
	return result;	
}


//------------------------------------------------------------------------------------------------
//	addOwners
//
//	Returns: YES if successful
//------------------------------------------------------------------------------------------------

-(BOOL)addOwners:(const char *)ownerList toEntry:(PWFileEntry *)inOutRec
{
	CFMutableDictionaryRef additionalDataDict = NULL;
	CFMutableArrayRef ownerArray = NULL;
	CFStringRef slotListString = NULL;
	CFArrayRef slotListArray = NULL;
	CFStringRef slotString = NULL;
	CFIndex slotListArrayIndex = 0;
	CFIndex slotListArrayCount = 0;
	CFRange ownerRange;
	int err = 0;
	char filePath[PATH_MAX] = {0};
	
	[self setFilePath:filePath maxPathSize:sizeof(filePath) forEntry:inOutRec];
	err = pwsf_loadxml( filePath, &additionalDataDict );
	if ( err == 0 && additionalDataDict != NULL )
	{
		slotListString = CFStringCreateWithCString( kCFAllocatorDefault, ownerList, kCFStringEncodingUTF8 );
		slotListArray = CFStringCreateArrayBySeparatingStrings( kCFAllocatorDefault, slotListString, CFSTR(",") );
		CFRelease( slotListString );
		
		if ( CFDictionaryGetValueIfPresent(additionalDataDict, CFSTR(kPWKey_ComputerAccountOwnerList), (const void **)&ownerArray) )
		{
			ownerRange = CFRangeMake( 0, CFArrayGetCount(ownerArray) );
			slotListArrayCount = CFArrayGetCount( slotListArray );
			for ( slotListArrayIndex = 0; slotListArrayIndex < slotListArrayCount; slotListArrayIndex++ )
			{
				slotString = (CFStringRef) CFArrayGetValueAtIndex( slotListArray, slotListArrayIndex );
				if ( !CFArrayContainsValue(ownerArray, ownerRange, slotString) )
					CFArrayAppendValue( ownerArray, slotString );
			}
		}
		else
		{
			CFDictionarySetValue( additionalDataDict, CFSTR(kPWKey_ComputerAccountOwnerList), slotListArray );
		}
		
		CFRelease( slotListArray );
	}
	else
	{
		additionalDataDict = pwsf_CreateAdditionalDataDictionaryWithOwners( ownerList );
	}
	if ( additionalDataDict != NULL ) {
		err = pwsf_savexml( filePath, additionalDataDict );
		CFRelease( additionalDataDict );
	}
	
	return (err == 0);
}


//------------------------------------------------------------------------------------------------
//	removeAllOwnersFromEntry
//
//	Returns: YES if successful
//------------------------------------------------------------------------------------------------

-(BOOL)removeAllOwnersFromEntry:(PWFileEntry *)inOutRec
{
	char filePath[PATH_MAX] = {0};
	
	[self setFilePath:filePath maxPathSize:sizeof(filePath) forEntry:inOutRec];
	return [self removeKey:CFSTR(kPWKey_ComputerAccountOwnerList) fromAdditionalDataFile:filePath];
}


//------------------------------------------------------------------------------------------------
//	isOwner
//
//	Returns: YES if successful
//------------------------------------------------------------------------------------------------

-(BOOL)isOwner:(const char *)user forEntry:(PWFileEntry *)inRec
{
	BOOL owner = NO;
	int err = 0;
	CFMutableDictionaryRef additionalDataDict = NULL;
	CFArrayRef ownerArray = NULL;
	CFStringRef userString = NULL;
	char filePath[PATH_MAX] = {0};
	
	[self setFilePath:filePath maxPathSize:sizeof(filePath) forEntry:inRec];
	err = pwsf_loadxml( filePath, &additionalDataDict );
	if ( err == 0 &&
		 additionalDataDict != NULL &&
		 CFDictionaryGetValueIfPresent(additionalDataDict, CFSTR(kPWKey_ComputerAccountOwnerList), (const void **)&ownerArray) )
	{
		userString = CFStringCreateWithCStringNoCopy( kCFAllocatorDefault, user, kCFStringEncodingUTF8, kCFAllocatorNull );
		if ( userString != NULL ) {
			owner = CFArrayContainsValue( ownerArray, CFRangeMake(0, CFArrayGetCount(ownerArray)), userString );
			CFRelease( userString );
		}
	}
	if ( additionalDataDict != NULL )
		CFRelease( additionalDataDict );
	
	return owner;
}


//------------------------------------------------------------------------------------------------
//	filePathForEntry
//------------------------------------------------------------------------------------------------

-(void)setFilePath:(char *)inOutPath maxPathSize:(size_t)inPathMax forEntry:(PWFileEntry *)inRec
{
	char adminID[35] = {0};
	
	pwsf_passwordRecRefToString( inRec, adminID );
	snprintf( inOutPath, inPathMax, "%s/%s/%s.plist", mDirPathStr, kPWAuxDirName, adminID );
}


//------------------------------------------------------------------------------------------------
//	removeKeyFromAdditionalDataFile
//
//	Returns: YES if successful
//------------------------------------------------------------------------------------------------

-(BOOL)removeKey:(CFStringRef)inKey fromAdditionalDataFile:(const char *)inFilePath
{
	CFMutableDictionaryRef additionalDataDict = NULL;
	int err = 0;
	
	err = pwsf_loadxml( inFilePath, &additionalDataDict );
	if ( err != 0 )
		return YES;		// no file, no key
	
	CFDictionaryRemoveValue( additionalDataDict, inKey );
	
	err = (CFDictionaryGetCount(additionalDataDict) > 0) ? pwsf_savexml( inFilePath, additionalDataDict ) : unlink( inFilePath );
	
	if ( additionalDataDict != NULL )
		CFRelease( additionalDataDict );
	
	return (err == 0);
}


#pragma mark -
#pragma mark REVERSE LOOKUPS
#pragma mark -

//------------------------------------------------------------------------------------------------
//	getUserIDFromName
//
//	Returns: Boolean (1==found, 0=not found)
//------------------------------------------------------------------------------------------------

-(int)getUserIDFromName:(const char *)inName anyUser:(BOOL)inAnyUser maxBuffSize:(long)inMaxBuffSize pwsID:(char *)outID
{
	PWFileHeader dbHeader;
	int result = 0;
	int err = 0;
	UInt32 index;
	PWFileEntry passRec;
	char theAdminID[256];
    long buffRemaining = inMaxBuffSize;
    long len;
    BOOL ignore;
	
    if ( outID == NULL || buffRemaining < 1 )
        return 0;
    
    *outID = '\0';
    buffRemaining--;
    
	if ( [self shouldTryLDAP] && [self getAccountIDFromLDAP:inName slotID:outID] )
	{
		if ( inAnyUser )
		{
			return 1;
		}
		else
		{
			// verify we got an admin
			pwsf_stringToPasswordRecRef( outID, &passRec );
			result = [self getValidPasswordRec:&passRec fromSpillBucket:&ignore unObfuscate:NO];
			if ( result == 0 && passRec.access.isAdminUser )
				return 1;
		}
	}
	
	err = [self getHeader:&dbHeader cachedCopyOK:NO];
	if ( err != 0 && err != -3 )
		return result;
	
	for ( index = dbHeader.deepestSlotUsed; index > 0; index-- )
	{
		// Not checking passwords, so leave them obfuscated for performance
		err = [self getPasswordRec:index putItHere:&passRec unObfuscate:NO];
		if ( err != 0 )
			break;
		
		if ( (inAnyUser || passRec.access.isAdminUser) && !passRec.access.isDisabled && strcmp( inName, passRec.usernameStr ) == 0 )
		{
            if ( result == 1 )
            {
                if ( buffRemaining < 1 )
                    break;
                
                strcat( outID, ";" );
                buffRemaining--;
            }
            
			pwsf_passwordRecRefToString( &passRec, theAdminID );
            len = strlen( theAdminID );
            
            if ( buffRemaining <= len )
                break;
            
            strcat( outID, theAdminID );
            buffRemaining -= len;
            
            result = 1;
		}
	}
		
	return result;
}


//------------------------------------------------------------------------------------------------
//	getUserIDFromPrincipal
//
//	Returns: Boolean (1==found, 0=not found)
//
//  If found, inOutUserRec contains the database record with the password field obfuscated.
//------------------------------------------------------------------------------------------------

-(int)getUserRecordFromPrincipal:(const char *)inPrincipal record:(PWFileEntry *)inOutUserRec
{
	PWFileHeader dbHeader;
	int err = 0;
	unsigned long index;
	PWFileEntry passRec;
	char *thePrincDomain = NULL;
    long len;
	BOOL ignore;
	char thePrincName[256];
	char slotID[35];
        
	// break principal into name and domain
	thePrincDomain = strchr( inPrincipal, '@' );
	if ( thePrincDomain == NULL )
		return 0;
	
	// must have a principal name
	len = thePrincDomain - inPrincipal;
	if ( len == 0 )
		return 0;
	
	// advance past the '@'
	thePrincDomain++;
	
	// save the name as a c-str
	strlcpy( thePrincName, inPrincipal, len + 1 );
	
	// Question: What about subdomains (dot in the principal name)?
	
	if ( [self shouldTryLDAP] )
	{
		err = [self getAccountIDFromLDAP:thePrincName slotID:slotID];
		if ( err == 1 )
		{
			pwsf_stringToPasswordRecRef( slotID, inOutUserRec );
			err = [self getValidPasswordRec:inOutUserRec fromSpillBucket:&ignore unObfuscate:NO];
			if ( err == 0 )
				return 1;
		}
	}
	
	err = [self getHeader:&dbHeader cachedCopyOK:YES];
	if ( err != 0 && err != -3 )
		return 0;
	
	for ( index = dbHeader.deepestSlotUsed; index > 0; index-- )
	{
		// Not checking passwords, so leave them obfuscated for performance
		err = [self getPasswordRec:index putItHere:&passRec unObfuscate:NO];
		if ( err != 0 )
			break;
		
		if ( strcmp( thePrincName, pwsf_GetPrincName(&passRec) ) == 0 )
		{
            memcpy( inOutUserRec, &passRec, sizeof(PWFileEntry) );
			return 1;
		}
	}
	
	return 0;
}


//------------------------------------------------------------------------------------------------
//	getLDAPSearchBase
//------------------------------------------------------------------------------------------------

-(char *)getLDAPSearchBase
{
	if ( mSearchBase == NULL )
	{
		char buffer[1024];
		char *argv[] = {"/usr/bin/ldapsearch", "-LLL", "-x", "-z", "1", "-b", "", "-s", "base", "namingContexts", NULL};
		int result = pwsf_LaunchTaskWithIO("/usr/bin/ldapsearch", argv, NULL, buffer, sizeof(buffer), NULL);
		if ( result == 0 )
		{
			char *tptr = strstr( buffer, "namingContexts: " );
			if ( tptr != NULL )
			{
				tptr += sizeof("namingContexts: ") - 1;
				mSearchBase = strdup( tptr );
				tptr = strchr( mSearchBase, '\n' );
				if ( tptr != NULL )
					*tptr = '\0';
			}
		}
	}
	
	return mSearchBase;
}


//------------------------------------------------------------------------------------------------
//	getAccountIDFromLDAP
//
//	RETURNS: 1=found, 0=not found
//------------------------------------------------------------------------------------------------

-(int)getAccountIDFromLDAP:(const char *)inUID slotID:(char *)outID
{
	char *searchBase = NULL;
	char *tptr = NULL;
	char filter[256];
	int found = 0;
	
	if ( inUID == NULL || outID == NULL )
		return 0;
	*outID = '\0';
	
	searchBase = [self getLDAPSearchBase];
	if ( searchBase != NULL )
	{
		int result = 0;
		char buffer[1024];
		char *argv[] = {"/usr/bin/ldapsearch", "-LLL", "-x", "-z", "1", "-b", searchBase, filter, "authAuthority", NULL};
		snprintf( filter, sizeof(filter), "(|(uid=%s)(cn=%s))", inUID, inUID );		
		result = pwsf_LaunchTaskWithIO("/usr/bin/ldapsearch", argv, NULL, buffer, sizeof(buffer), NULL);
		if ( result == 0 )
		{
			tptr = strstr( buffer, "authAuthority: ;ApplePasswordServer;" );
			if ( tptr != NULL )
			{
				tptr += sizeof("authAuthority: ;ApplePasswordServer;") - 1;
				*(tptr + 34) = '\0';
				strcpy( outID, tptr );
				return 1;
			}
		}
	}
	
	return found;
}


// --------------------------------------------------------------------------------
//	shouldTryLDAP
// --------------------------------------------------------------------------------

-(int)shouldTryLDAP
{
	PWFileHeader dbHeader;
	
	return ( [self getHeader:&dbHeader cachedCopyOK:YES] == 0 && dbHeader.deepestSlotUsed > 100 );
}


#pragma mark -
#pragma mark PASSWORD UTILS
#pragma mark -

//------------------------------------------------------------------------------------------------
//	requireNewPasswordForAllAccounts
//------------------------------------------------------------------------------------------------

-(void)requireNewPasswordForAllAccounts:(BOOL)inRequireNew
{
	PWFileHeader dbHeader;
	int err = 0;
	unsigned long index;
	PWFileEntry passRec;
        
	err = [self getHeader:&dbHeader cachedCopyOK:YES];
	if ( err != 0 && err != -3 )
		return;
	
	for ( index = dbHeader.deepestSlotUsed; index > 0; index-- )
	{
		// Not checking passwords, so leave them obfuscated for performance
		err = [self getPasswordRec:index putItHere:&passRec unObfuscate:NO];
		if ( err != 0 )
			continue;
		
		passRec.access.newPasswordRequired = inRequireNew;
		
		[self setPassword:&passRec atSlot:index obfuscate:NO setModDate:YES];
	}
}


//------------------------------------------------------------------------------------------------
//	kerberizeOrRequireNewPasswordForAllAccounts
//------------------------------------------------------------------------------------------------

-(void)kerberizeOrRequireNewPasswordForAllAccounts
{
	PWFileHeader dbHeader;
	int err = 0;
	UInt32 index;
	PWFileEntry passRec;
	
	err = [self getHeader:&dbHeader cachedCopyOK:YES];
	if ( err != 0 && err != -3 )
		return;
	
	for ( index = dbHeader.deepestSlotUsed; index > 0; index-- )
	{
		// Get obfuscated record. Performance is better for the passwordIsHash case
		err = [self getPasswordRec:index putItHere:&passRec unObfuscate:NO];
		if ( err != 0 || PWRecIsZero(passRec) || passRec.extraAccess.recordIsDead )
			continue;
		
		if ( passRec.access.passwordIsHash )
		{
			// require new
			passRec.access.newPasswordRequired = 1;
			[self setPassword:&passRec atSlot:index obfuscate:NO setModDate:YES];
		}
		else
		{
			// kerberize
			
			// un-obfuscate the password
			pwsf_DESAutoDecode( passRec.passwordStr );
			
			if ( pwsf_AddPrincipal(pwsf_GetPrincName(&passRec), passRec.passwordStr, passRec.digest[kPWHashSlotKERBEROS].digest,
				 sizeof(passRec.digest[kPWHashSlotKERBEROS].digest)) == 0 )
			{
				if ( passRec.digest[kPWHashSlotKERBEROS].digest[0] != '\0' )
				{
					strcpy( passRec.digest[kPWHashSlotKERBEROS].method, "KerberosRealmName" );
					// if the method field isn't assigned, then the pwsf_GetPrincName() function returns username
					strlcpy( passRec.digest[kPWHashSlotKERBEROS_NAME].digest, pwsf_GetPrincName(&passRec),
								sizeof(passRec.digest[kPWHashSlotKERBEROS_NAME].digest) );
					strcpy( passRec.digest[kPWHashSlotKERBEROS_NAME].method, "KerberosPrincName" );
				}
				else
				{
					passRec.digest[kPWHashSlotKERBEROS].method[0] = 0;
				}
				[self setPassword:&passRec atSlot:index];
			}
		}
		bzero( &passRec, sizeof(passRec) );
	}
	
	[mOverflow kerberizeOrNewPassword];
}


//------------------------------------------------------------------------------------------------
//	addPasswordForUser
//------------------------------------------------------------------------------------------------

-(int)addPasswordForUser:(const char *)inUser password:(const char *)inPassword pwsID:(char *)outPasswordRef
{
    int result;
    PWFileEntry anEntry;
    char refStr[256];
    
	if ( strlen(inPassword) > sizeof(anEntry.passwordStr) - 1 )
		return kAuthPasswordTooLong;
	
	bzero( &anEntry, sizeof(anEntry) );
	
    anEntry.access.isDisabled = NO;
    anEntry.access.isAdminUser = NO;
    anEntry.access.newPasswordRequired = NO;
    anEntry.access.usingHistory = NO;
    anEntry.access.canModifyPasswordforSelf = YES;
    anEntry.access.usingExpirationDate = NO;
    anEntry.access.usingHardExpirationDate = NO;
    anEntry.access.requiresAlpha = NO;
    anEntry.access.requiresNumeric = NO;
    anEntry.access.passwordIsHash = NO;
	
    anEntry.access.maxMinutesOfNonUse = 0;
    anEntry.access.maxFailedLoginAttempts = 0;
    anEntry.access.minChars = 0;
    anEntry.access.maxChars = 0;
    
    strcpy( anEntry.usernameStr, inUser );
    strlcpy( anEntry.passwordStr, inPassword, sizeof(anEntry.passwordStr) );
 
	result = [self addPassword:&anEntry obfuscate:YES];
	
    pwsf_passwordRecRefToString( &anEntry, refStr );
    strcpy( outPasswordRef, refStr );
    
    return result;
}


//------------------------------------------------------------------------------------------------
//	NewPasswordSlot
//
//	Returns: errno
//	Similar to AddPassword() but faster for batch operations. 
//	Calls initPasswordRecord() which does not save the password record.
//	By default, use the AddPassword() method.
//------------------------------------------------------------------------------------------------

-(int)newPasswordSlot:(const char *)inUser
	password:(const char *)inPassword
	pwsID:(char *)outPasswordRef
	pwsRec:(PWFileEntry *)inOutUserRec
{
	int result;

	if ( inUser == NULL || inPassword == NULL || outPasswordRef == NULL || inOutUserRec == NULL )
		return -1;

	if ( strlen(inPassword) > 511 )
		return kAuthPasswordTooLong;
	
	bzero( inOutUserRec, sizeof(PWFileEntry) );
	inOutUserRec->access.canModifyPasswordforSelf = YES;
	
	strcpy( inOutUserRec->usernameStr, inUser );
	strcpy( inOutUserRec->passwordStr, inPassword );
	
	result = [self initPasswordRecord:inOutUserRec obfuscate:YES];
	
	pwsf_passwordRecRefToString( inOutUserRec, outPasswordRef );

	return result;
}

#pragma mark -
#pragma mark REPLICATION
#pragma mark -

//------------------------------------------------------------------------------------------------
//	getSyncTime:fromSyncFile
//
//	Returns: 0 == success, -1 == fail
//
//	This method gets the official sync time (watermark) for a data file
//------------------------------------------------------------------------------------------------

-(int)getSyncTime:(time_t *)outSyncTime fromSyncFile:(const char *)inSyncFile 
{
	int err;
	FILE *syncFile;
	int readCount;
	unsigned long remoteFileLen;
	struct stat sb;
	time_t syncTime = 0;
	
	// sanity
    if ( inSyncFile == NULL || outSyncTime == NULL )
		return -1;
	
	*outSyncTime = 0;
	
	// get file len
    remoteFileLen = 0;
    err = lstat( inSyncFile, &sb );
    if ( err == 0 )
        remoteFileLen = sb.st_size;
    if ( remoteFileLen < sizeof(PWFileHeader) )
		return -1;
	
	// open the sync file
	syncFile = fopen( inSyncFile, "r" );
    if ( syncFile == NULL )
		return -1;
	
	// read opposing sync time
	readCount = fread( &syncTime, sizeof(syncTime), 1, syncFile );
	if ( readCount == 1 )
		*outSyncTime = syncTime;
	
	fclose( syncFile );
	
	return 0;
}


//------------------------------------------------------------------------------------------------
//	makeSyncFile
//
//  Returns: 0 = success, -1 fail
//
//  <inKerberosRecordLimit>		0=no limit; otherwise, database size beyond which kerberos sync
//								is queued
//------------------------------------------------------------------------------------------------

-(int)makeSyncFile:(const char *)inFileName
	afterDate:(time_t)inAfterDate
	timeSkew:(long)inTimeSkew
	numRecordsUpdated:(long *)outNumRecordsUpdated
	kerberosRecordLimit:(unsigned int)inKerberosRecordLimit
	kerberosOmitted:(BOOL *)outKerberosOmitted
	passwordServerOmitted:(BOOL *)outPasswordServerOmitted
{
	PWFileHeader dbHeader;
	int result = 0;
	int err;
	UInt32 index;
	PWFileEntry passRec;
	PWSFKerberosPrincipal* kerberosRec;
    time_t theTime;
	FILE *syncFile;
	int writeCount;
	int zeroLen = 0;
	PWSFKerberosPrincipalList kerbList;
	bool addKerberosRecords = true;
	
	// sanity
    if ( inFileName == NULL )
		return -1;

	if ( outNumRecordsUpdated != NULL )
		*outNumRecordsUpdated = 0;
	if ( outKerberosOmitted != NULL )
		*outKerberosOmitted = false;
	if ( outPasswordServerOmitted != NULL )
		*outPasswordServerOmitted = false;
		
	// create sync file
	syncFile = fopen( inFileName, "w+" );
    if ( syncFile == NULL )
		return -1;
	
	err = chmod( inFileName, S_IRUSR | S_IWUSR );
	
	// load/copy header
	do
	{
		writeCount = fwrite( &inAfterDate, sizeof(inAfterDate), 1, syncFile );
		if ( writeCount != 1 ) {
			result = -1;
			break;
		}
		
		err = [self getHeader:&dbHeader];
		if ( err != 0 && err != -3 ) {
			result = err;
			break;
		}
		
		writeCount = fwrite( &dbHeader, sizeof(dbHeader), 1, syncFile );
		if ( writeCount != 1 ) {
			result = -1;
			break;
		}
				
		if ( inKerberosRecordLimit > 0 && dbHeader.deepestSlotUsed > inKerberosRecordLimit )
			addKerberosRecords = false;
		
		if ( outPasswordServerOmitted != NULL && dbHeader.deepestSlotUsed > 4176 )
		{
			*outPasswordServerOmitted = true;
		}
		else
		{
			if ( addKerberosRecords )
				kerbList.ReadAllPrincipalsFromDB( inAfterDate );
			
			// copy records after the sync date
			for ( index = dbHeader.deepestSlotUsed; index > 0; index-- )
			{				
				err = [self getPasswordRec:index putItHere:&passRec unObfuscate:NO];
				if ( err != 0 ) {
					result = err;
					break;
				}
				if ( PWRecIsZero(passRec) )
					continue;
				
				// adjust time skew for comparison purposes. The record itself is
				// adjusted on the processing side.
				theTime = timegm( (struct tm *)&passRec.modificationDate ) + inTimeSkew;
				if ( theTime >= inAfterDate )
				{
					// replicate the identity to avoid collisions, but blank out any useful data
					if ( passRec.extraAccess.doNotReplicate )
					{
						bzero( &passRec.passwordStr, sizeof(passRec.passwordStr) );
						bzero( &passRec.digest[0], sizeof(PasswordDigest) * kPWFileMaxDigests );
						bzero( &passRec.userdata, sizeof(passRec.userdata) );
						bzero( &passRec.extraAccess.userkey, sizeof(passRec.extraAccess.userkey) );
						
						// set passwordIsHash to guarantee that the plain text field is useless
						passRec.access.passwordIsHash = 1;
					}
					
					writeCount = fwrite( &passRec, sizeof(passRec), 1, syncFile );
					if ( writeCount != 1 ) {
						result = -1;
						break;
					}
										
					kerberosRec = NULL;
					if ( addKerberosRecords && passRec.digest[kPWHashSlotKERBEROS].digest[0] != '\0' )
					{
						char principalName[600] = {0,};
						
						// kerberos records only need to be sent if the password changed
						theTime = timegm( (struct tm *)&passRec.modDateOfPassword ) + inTimeSkew;
						if ( theTime >= inAfterDate )
						{
							strcpy(principalName, pwsf_GetPrincName(&passRec));
							strcat(principalName, "@");
							strcat(principalName, passRec.digest[kPWHashSlotKERBEROS].digest);
							kerberosRec = kerbList.GetPrincipalByName(principalName);
						}
					}
					if (kerberosRec != NULL) {
						writeCount = kerberosRec->WritePrincipalToFile(syncFile);
						delete kerberosRec;
					}
					else {
						writeCount = fwrite( &zeroLen, sizeof(zeroLen), 1, syncFile );
					}
					if ( writeCount != 1 ) {
						result = -1;
						break;
					}
					
					if ( outNumRecordsUpdated != NULL )
						(*outNumRecordsUpdated)++;
				}
			}
			
			// look in the spill-bucket
			[mOverflow addOverflowToSyncFile:syncFile afterDate:inAfterDate timeSkew:inTimeSkew numUpdated:outNumRecordsUpdated];
			
			if ( addKerberosRecords )
			{
				// Now add remaining Kerberos records since modDate, since these records don't have
				// a corresponding password server record, just put an invalid slot number there
				bzero(&passRec, sizeof(passRec));
				passRec.slot = (UInt32)-1;
				
				index = 0;
				kerberosRec = kerbList.GetPrincipalByIndex(index++);
				while (kerberosRec != NULL)
				{
					writeCount = fwrite( &passRec, sizeof(passRec), 1, syncFile );
					if ( writeCount != 1 ) {
						result = -1;
						break;
					}
					writeCount = kerberosRec->WritePrincipalToFile(syncFile);
					if ( writeCount != 1 ) {
						result = -1;
						break;
					}
					kerberosRec = kerbList.GetPrincipalByIndex(index++);
				}
			}
			
			// TODO:add additional-data files
			
		}
	}
	while (0);
	
	fclose( syncFile );
	if ( result != 0 )
		unlink( inFileName );
	
	bzero( &dbHeader, sizeof(dbHeader) );
	bzero( &passRec, sizeof(passRec) );
	
	if ( outKerberosOmitted != NULL )
		*outKerberosOmitted = (!addKerberosRecords);
	
	return result;
}


//------------------------------------------------------------------------------------------------
//	processSyncFile
//
//	Returns: 0 == success, -1 == fail, -2 == incompatible databases, -3 == db file busy
//
//	This method processes the records from a remote parent or replica. Any local records
//	believed to be newer than the remote ones are preserved.
//------------------------------------------------------------------------------------------------

-(int)processSyncFile:(const char *)inSyncFile
	timeSkew:(long)inTimeSkew
	numAccepted:(long *)outNumAccepted
	numTrumped:(long *)outNumTrumped
{
	int result = 0;
	int err;
	FILE *syncFile;
	time_t localTime, remoteTime;
	PWFileHeader localHeader, remoteHeader;
	PWFileEntry localRec, remoteRec;
    int readCount;
	unsigned long remoteFileLen = 0;
	struct stat sb;
	time_t syncTime = 0;
	BOOL bFromSpillBucket;
	bool bNeedsUpdate;
	bool haveLock;
	bool kerberosAliveAndBarking = true;
	int kerbRecordLen;
	char *kerbNamePtr = NULL;
	PWSFKerberosPrincipal* remoteKerbRec;
	PWSFKerberosPrincipal* localKerbRec;
	PWSFKerberosPrincipalList kerbList;
	PWSFKerberosPrincipalList localKerbList;
	char *kerbCmdBuffer = NULL;
	int kerbCmdBufferSize = 0;
	
	[self setForceSync:NO];
	[self setShouldSyncNow:NO];
	
	// sanity
    if ( inSyncFile == NULL )
		return -1;
	
	if ( outNumAccepted != NULL )
		*outNumAccepted = 0;
	if ( outNumTrumped != NULL )
		*outNumTrumped = 0;
	
	// get file len
    remoteFileLen = 0;
    err = lstat( inSyncFile, &sb );
    if ( err == 0 )
        remoteFileLen = sb.st_size;
    if ( remoteFileLen < sizeof(PWFileHeader) )
		return -1;
	
	// open the sync file
	syncFile = fopen( inSyncFile, "r" );
    if ( syncFile == NULL )
		return -1;
	
	[self pwWait];
	haveLock = [self pwLock:kLongerLockInterval];
	[self pwSignal];
	
	do
	{
		// Giving up is not fatal
		// The data can be retrieved at the next sync
		if ( ! haveLock ) {
			result = -3;
			break;
		}
		
		// copy our header
		err = [self getHeader:&localHeader];
		if ( err != 0 && err != -3 ) {
			result = err;
			break;
		}
		
		// read opposing sync time
		readCount = fread( &syncTime, sizeof(syncTime), 1, syncFile );
		if ( readCount != 1 ) {
			result = -1;
			break;
		}
		
		// read opposing header
		readCount = fread( &remoteHeader, sizeof(remoteHeader), 1, syncFile );
		if ( readCount != 1 ) {
			result = -1;
			break;
		}
				
		// check compatibility
		if ( localHeader.signature != remoteHeader.signature ||
			 localHeader.version != remoteHeader.version ||
			 localHeader.entrySize != remoteHeader.entrySize )
		{
			result = -2;
			break;
		}
		
		// sync the header
		localHeader.sequenceNumber = MAX(localHeader.sequenceNumber, remoteHeader.sequenceNumber);
		localHeader.deepestSlotUsed = MAX(localHeader.deepestSlotUsed, remoteHeader.deepestSlotUsed);
		
		if ( remoteHeader.accessModDate > 0 && remoteHeader.accessModDate - inTimeSkew > localHeader.accessModDate ) {
			localHeader.access = remoteHeader.access;
			localHeader.extraAccess = remoteHeader.extraAccess;
			localHeader.accessModDate = remoteHeader.accessModDate - inTimeSkew;
		}
		
		err = [self setHeader:&localHeader];
		
		if ( remoteHeader.numberOfSlotsCurrentlyInFile > localHeader.numberOfSlotsCurrentlyInFile )
			[self expandDatabase:remoteHeader.numberOfSlotsCurrentlyInFile - localHeader.numberOfSlotsCurrentlyInFile nextAvailableSlot:NULL];
		
		[self pwUnlock];
		
		localKerbList.ReadAllPrincipalsFromDB( 0, mKerberosCacheLimit );
		
		// either update the record or trump it
		while (true)
		{
			bNeedsUpdate = false;
			
			readCount = fread( &remoteRec, sizeof(remoteRec), 1, syncFile );
			if ( readCount != 1 )
				break;
			
			readCount = fread( &kerbRecordLen, sizeof(kerbRecordLen), 1, syncFile );
			if ( readCount != 1 )
				break;
				
			localKerbRec = NULL;
			if (kerbRecordLen > 0)
			{
				// always read to advance the file position, then toss if kerberos is OFF.
				remoteKerbRec = kerbList.ReadPrincipalFromFile(syncFile, kerbRecordLen);
				if ( ! kerberosAliveAndBarking ) {
					delete remoteKerbRec;
					remoteKerbRec = NULL;
				}
			}
			else
				remoteKerbRec = NULL;
			
			if (remoteKerbRec != NULL)
			{
				localKerbRec = localKerbList.GetPrincipalByName( remoteKerbRec->GetName() );
				if ( localKerbRec == NULL )
				{
					err = PWSFKerberosPrincipal::ReadPrincipalFromDB(remoteKerbRec->GetName(), &localKerbRec);
					if ( err == -4 )
						kerberosAliveAndBarking = false;
				}
			}
			
			localRec = remoteRec;
			
			if (remoteRec.slot != (UInt32)-1)
			{	
				// this is a normal user record
				err = [self getValidPasswordRec:&localRec fromSpillBucket:&bFromSpillBucket unObfuscate:NO];
				if ( err == 0 && bFromSpillBucket )
				{
					PWFileEntry mainRec;
					
					// if the main slot has a death certificate, replace it.
					err = [self getPasswordRec:localRec.slot putItHere:&mainRec unObfuscate:NO];
					if ( err == 0 )
					{
						if ( PWRecIsZero(mainRec) )
						{
							// if the main slot is empty, delete the slot in the overflow
							// (it will get written into the main db)
							[mOverflow deleteSlot:&localRec];
						}
						else if ( mainRec.extraAccess.recordIsDead && !localRec.extraAccess.recordIsDead )
						{
							// replace death certificates with the new record
							if ( [self setPassword:&localRec atSlot:localRec.slot obfuscate:NO setModDate:NO] == 0 )
							{
								[mOverflow deleteSlot:&localRec];
								
								// keep the death certificate in the overflow. It will
								// get cleaned up eventually.
								[mOverflow saveOverflowRecord:&mainRec obfuscate:NO setModDate:NO];
							}
						}
					}
				}
				
				if ( err == 0 && localRec.extraAccess.doNotReplicate )
					continue;
				
				if ( err != 0 )
				{
					if ( remoteRec.extraAccess.recordIsDead )
					{
						bzero( &localRec, sizeof(localRec) );
					}
					else
					{
						// record not in the database yet
						localRec = remoteRec;
						bNeedsUpdate = true;
					}
				}
				else if ( localKerbRec == NULL )
				{
					// if we couldn't load the kerberos record earlier,
					// let's see if the principal exists locally because we
					// may need to delete it.
					char princ[256] = {0,};
					
					if ( localRec.digest[4].digest[0] != 0 &&
						strcmp(localRec.digest[4].method, "KerberosRealmName") == 0 )
					{
						snprintf( princ, sizeof(princ), "%s@%s", pwsf_GetPrincName(&localRec), localRec.digest[4].digest );
						localKerbRec = localKerbList.GetPrincipalByName( princ );
						if ( localKerbRec == NULL )
						{
							err = PWSFKerberosPrincipal::ReadPrincipalFromDB(princ, &localKerbRec);
							if ( err == -4 )
								kerberosAliveAndBarking = false;
						}
					}
				}
				
				// recordIsDead needs to be an OR, not last mod date or we get the undead
				if ( localRec.extraAccess.recordIsDead != remoteRec.extraAccess.recordIsDead )
				{
					localRec.extraAccess.recordIsDead |= remoteRec.extraAccess.recordIsDead;
					bNeedsUpdate = true;
				}
				
				// password fields
				localTime = timegm( (struct tm *)&localRec.modDateOfPassword );
				remoteTime = timegm( (struct tm *)&remoteRec.modDateOfPassword ) - inTimeSkew;
				if ( inTimeSkew != 0 )
					gmtime_r( &remoteTime, (struct tm *)&remoteRec.modDateOfPassword );
				
				if ( remoteTime > localTime )
				{
					memcpy( &localRec.modDateOfPassword, &remoteRec.modDateOfPassword, sizeof(BSDTimeStructCopy) );
					memcpy( localRec.passwordStr, remoteRec.passwordStr, sizeof(localRec.passwordStr) );
					for ( int idx = 0; idx < kPWFileMaxDigests; idx++ )
						localRec.digest[idx] = remoteRec.digest[idx];
						
					bNeedsUpdate = true;
				}
				else if (localKerbRec != NULL && remoteKerbRec != NULL)
					remoteKerbRec->CopyPassword(localKerbRec);
				
				// last login time
				localTime = timegm( (struct tm *)&localRec.lastLogin );
				remoteTime = timegm( (struct tm *)&remoteRec.lastLogin );
				if ( remoteTime > inTimeSkew )
					remoteTime -= inTimeSkew;
				if ( inTimeSkew != 0 )
					gmtime_r( &remoteTime, (struct tm *)&remoteRec.lastLogin );
				
				if ( remoteTime > localTime )
				{
					memcpy( &localRec.lastLogin, &remoteRec.lastLogin, sizeof(BSDTimeStructCopy) );
					bNeedsUpdate = true;
				}
				else if (localKerbRec != NULL && remoteKerbRec != NULL)
					remoteKerbRec->CopyLastLogin(localKerbRec);
				
				// all non-special fields
				localTime = timegm( (struct tm *)&localRec.modificationDate );
				remoteTime = timegm( (struct tm *)&remoteRec.modificationDate ) - inTimeSkew;
				if ( inTimeSkew != 0 )
					gmtime_r( &remoteTime, (struct tm *)&remoteRec.modificationDate );
				
				if ( remoteTime > localTime )
				{
					// policy updates for kerberos
					if ( localKerbRec != NULL && memcmp(&localRec.access, &remoteRec.access, sizeof(PWAccessFeatures)) != 0 )
					{
						pwsf_ModifyPrincipalWithBuffer( localKerbRec->GetName(), &remoteRec.access,
							localRec.access.maxMinutesUntilChangePassword, &kerbCmdBuffer, &kerbCmdBufferSize );
					}
					
					memcpy( &localRec.modificationDate, &remoteRec.modificationDate, sizeof(BSDTimeStructCopy) );
					memcpy( &localRec.access, &remoteRec.access, sizeof(PWAccessFeatures) );
					memcpy( &localRec.usernameStr, &remoteRec.usernameStr, sizeof(localRec.usernameStr) );
					memcpy( &localRec.userdata, &remoteRec.userdata, sizeof(localRec.userdata) );
					localRec.failedLoginAttempts = remoteRec.failedLoginAttempts;
					localRec.extraAccess.doNotReplicate = remoteRec.extraAccess.doNotReplicate;
					
					bNeedsUpdate = true;
				}
				
				// do not replicate deleted users back into Kerberos DB
				if ( localRec.extraAccess.recordIsDead )
				{
					if ( remoteKerbRec != NULL )
						delete remoteKerbRec;
					if ( localKerbRec != NULL && (kerbNamePtr=localKerbRec->GetName()) )
					{
						pwsf_DeletePrincipal( kerbNamePtr );
					}
				}
				
				if ( bNeedsUpdate )
				{
					if ( outNumAccepted != NULL )
						(*outNumAccepted)++;
					
					[self pwWait];
					haveLock = [self pwLock:kLongerLockInterval];
					[self pwSignal];
					
					if ( ! haveLock )
					{
						// Note: this only breaks out of the inner while loop.
						result = -3;
						break;
					}
					
					err = [self addPassword:&localRec atSlot:localRec.slot obfuscate:NO setModDate:NO];
				}
				else
				{
					if ( remoteKerbRec != NULL )
						delete remoteKerbRec;	// remove from list to update
					
					if ( outNumTrumped != NULL )
						(*outNumTrumped)++;
				}
			}
			else if (remoteKerbRec != NULL)
			{
				// this is a non-user kerberos record.  We just check the mod-date for a winner.
				// If the local kerb record exists and has a moddate at least as recent as the
				// remote record, then remove the record from the list to update.
				if ((localKerbRec != NULL) && (remoteKerbRec->GetRecordModDate() <= localKerbRec->GetRecordModDate()))
					delete remoteKerbRec;
			}
			
			// remaining remote kerberos records will be delete along with the kerbList
			if (localKerbRec != NULL)
				delete localKerbRec;
		}
			
		// now sync kerberos information
		if ( kerberosAliveAndBarking )
			kerbList.WriteAllPrincipalsToDB();
	}
	while (0);
	
	[self pwUnlock];
	
	fclose( syncFile );
	
	if ( kerbCmdBuffer != NULL ) {
		free( kerbCmdBuffer );
		kerbCmdBuffer = NULL;
		kerbCmdBufferSize = 0;
	}

	// clear sensitive info
	bzero( &localHeader, sizeof(localHeader) );
	bzero( &remoteHeader, sizeof(remoteHeader) );
	bzero( &localRec, sizeof(localRec) );
	bzero( &remoteRec, sizeof(remoteRec) );
	
	return result;
}


//------------------------------------------------------------------------------------------------
//	mergeHeader
//
//	Returns: 0 == success, -1 == fail, -2 == incompatible databases, -3 == db file busy
//
//  Updates the database header
//------------------------------------------------------------------------------------------------

-(int)mergeHeader:(PWFileHeader *)inRemoteHeader timeSkew:(long)inTimeSkew
{
	int result = 0;
	int err;
	PWFileHeader localHeader;
	bool haveLock;
	
	// sanity
    if ( inRemoteHeader == NULL )
		return -1;
	
	// grab file lock
	[self pwWait];
	haveLock = [self pwLock:kLongerLockInterval];
	[self pwSignal];
	
	do
	{
		// Giving up is not fatal
		// The data can be retrieved at the next sync
		if ( ! haveLock )
		{
			syslog( LOG_ALERT, "[AuthDBFile mergeHeader] - can't get file lock");
			result = -3;
			break;
		}
		
		// copy our header
		err = [self getHeader:&localHeader];
		if ( err != 0 && err != -3 ) {
			result = err;
			break;
		}
		
		// check compatibility
		if ( localHeader.signature != inRemoteHeader->signature ||
			 localHeader.version != inRemoteHeader->version ||
			 localHeader.entrySize != inRemoteHeader->entrySize )
		{
			result = -2;
			break;
		}
		
		// sync the header
		localHeader.sequenceNumber = MAX(localHeader.sequenceNumber, inRemoteHeader->sequenceNumber);
		localHeader.deepestSlotUsed = MAX(localHeader.deepestSlotUsed, inRemoteHeader->deepestSlotUsed);
		
		// add time skew adjustment
		if ( inRemoteHeader->accessModDate > 0 && inRemoteHeader->accessModDate - inTimeSkew > localHeader.accessModDate ) {
			localHeader.access = inRemoteHeader->access;
			localHeader.extraAccess = inRemoteHeader->extraAccess;
			localHeader.accessModDate = inRemoteHeader->accessModDate - inTimeSkew;
		}
		
		err = [self setHeader:&localHeader];
		
		if ( inRemoteHeader->numberOfSlotsCurrentlyInFile > localHeader.numberOfSlotsCurrentlyInFile )
			[self expandDatabase:inRemoteHeader->numberOfSlotsCurrentlyInFile - localHeader.numberOfSlotsCurrentlyInFile
				nextAvailableSlot:NULL];
	}
	while (0);
	
	[self pwUnlock];
	
	// clear sensitive info
	bzero( &localHeader, sizeof(localHeader) );
	
	return result;
}


//------------------------------------------------------------------------------------------------
//	mergeSlot
//
//	Returns: 0 == success, -1 == fail, -2 == incompatible databases, -3 == db file busy
//
//  Updates the database account records; assumes the header has been updated separately
//------------------------------------------------------------------------------------------------

-(SyncStatus)mergeSlot:(PWFileEntry *)inRemoteRecord
	timeSkew:(long)inTimeSkew
	accepted:(BOOL *)outAccepted
{
	SyncStatus result = kSyncStatusNoErr;
	int err = 0;
	time_t localTime = 0;
	time_t remoteTime = 0;
	PWFileEntry localRec = {0};
	BOOL bFromSpillBucket = NO;
	BOOL bNeedsUpdate = NO;
	BOOL haveLock = NO;
	
	if ( outAccepted != NULL )
		*outAccepted = NO;
	
	do
	{
		if ( inRemoteRecord->slot != (UInt32)-1 )
		{
			// this is a normal user record
			localRec = *inRemoteRecord;
			err = [self getValidPasswordRec:&localRec fromSpillBucket:&bFromSpillBucket unObfuscate:NO];
			if ( err == 0 && bFromSpillBucket )
			{
				PWFileEntry mainRec;
				
				// if the main slot has a death certificate, replace it.
				err = [self getPasswordRec:localRec.slot putItHere:&mainRec unObfuscate:NO];
				if ( err == 0 )
				{
					if ( PWRecIsZero(mainRec) )
					{
						// if the main slot is empty, delete the slot in the overflow
						// (it will get written into the main db)
						[mOverflow deleteSlot:&localRec];
					}
					else if ( mainRec.extraAccess.recordIsDead && !localRec.extraAccess.recordIsDead )
					{
						// replace death certificates with the new record
						if ( [self setPassword:&localRec atSlot:localRec.slot obfuscate:NO setModDate:NO] == 0 )
						{
							[mOverflow deleteSlot:&localRec];
							
							// keep the death certificate in the overflow. It will
							// get cleaned up eventually.
							[mOverflow saveOverflowRecord:&mainRec obfuscate:NO setModDate:NO];
						}
					}
				}
			}
			
			if ( err == 0 && localRec.extraAccess.doNotReplicate )
				continue;
			
			if ( err != 0 )
			{
				if ( inRemoteRecord->extraAccess.recordIsDead )
				{
					bzero( &localRec, sizeof(localRec) );
				}
				else
				{
					// record not in the database yet
					localRec = *inRemoteRecord;
					bNeedsUpdate = YES;
				}
			}
			
			// recordIsDead needs to be an OR, not last mod date or we get the undead
			if ( localRec.extraAccess.recordIsDead != inRemoteRecord->extraAccess.recordIsDead )
			{
				char princ[512];
				
				localRec.extraAccess.recordIsDead = 1;
				bNeedsUpdate = YES;
				
				// remove the kerberos principal
				if ( inRemoteRecord->extraAccess.recordIsDead &&
					 inRemoteRecord->digest[kPWHashSlotKERBEROS].digest[0] != '\0' )
				{
					snprintf( princ, sizeof(princ), "%s@%s",
								pwsf_GetPrincName(inRemoteRecord),
								inRemoteRecord->digest[kPWHashSlotKERBEROS].digest );
					pwsf_DeletePrincipal( princ );
				}
			}
			
			// password fields
			localTime = timegm( (struct tm *)&localRec.modDateOfPassword );
			remoteTime = timegm( (struct tm *)&inRemoteRecord->modDateOfPassword ) - inTimeSkew;
			if ( inTimeSkew != 0 )
				gmtime_r( &remoteTime, (struct tm *)&inRemoteRecord->modDateOfPassword );
				
			if ( remoteTime > localTime )
			{
				memcpy( &localRec.modDateOfPassword, &inRemoteRecord->modDateOfPassword, sizeof(BSDTimeStructCopy) );
				memcpy( localRec.passwordStr, inRemoteRecord->passwordStr, sizeof(localRec.passwordStr) );
				for ( int idx = 0; idx < kPWFileMaxDigests; idx++ )
					localRec.digest[idx] = inRemoteRecord->digest[idx];
						
				bNeedsUpdate = YES;
			}
				
			// last login time
			localTime = timegm( (struct tm *)&localRec.lastLogin );
			remoteTime = timegm( (struct tm *)&inRemoteRecord->lastLogin );
			if ( remoteTime > inTimeSkew )
				remoteTime -= inTimeSkew;
			if ( inTimeSkew != 0 )
				gmtime_r( &remoteTime, (struct tm *)&inRemoteRecord->lastLogin );
				
			if ( remoteTime > localTime )
			{
				memcpy( &localRec.lastLogin, &inRemoteRecord->lastLogin, sizeof(BSDTimeStructCopy) );
				bNeedsUpdate = YES;
			}

			// all non-special fields
			localTime = timegm( (struct tm *)&localRec.modificationDate );
			remoteTime = timegm( (struct tm *)&inRemoteRecord->modificationDate ) - inTimeSkew;
			if ( inTimeSkew != 0 )
				gmtime_r( &remoteTime, (struct tm *)&inRemoteRecord->modificationDate );
				
			if ( remoteTime > localTime )
			{
				memcpy( &localRec.modificationDate, &inRemoteRecord->modificationDate, sizeof(BSDTimeStructCopy) );
				memcpy( &localRec.access, &inRemoteRecord->access, sizeof(PWAccessFeatures) );
				memcpy( &localRec.extraAccess, &inRemoteRecord->extraAccess, sizeof(PWMoreAccessFeatures) );
				memcpy( &localRec.usernameStr, &inRemoteRecord->usernameStr, sizeof(localRec.usernameStr) );
				memcpy( &localRec.userGUID, &inRemoteRecord->userGUID, sizeof(localRec.userGUID) );
				memcpy( &localRec.admingroup, &inRemoteRecord->admingroup, sizeof(localRec.admingroup) );
				memcpy( &localRec.userdata, &inRemoteRecord->userdata, sizeof(localRec.userdata) );
				memcpy( &localRec.disableReason, &inRemoteRecord->disableReason, sizeof(localRec.disableReason) );
				localRec.failedLoginAttempts = inRemoteRecord->failedLoginAttempts;
				
				bNeedsUpdate = YES;
			}
			
			if ( bNeedsUpdate )
			{
				if ( outAccepted != NULL )
					*outAccepted = YES;
					
				[self pwWait];
				haveLock = [self pwLock:kLongerLockInterval];
				[self pwSignal];
				
				if ( ! haveLock )
				{
					result = kSyncStatusServerDatabaseBusy;
					break;
				}
				
				err = [self addPassword:&localRec atSlot:localRec.slot obfuscate:NO setModDate:NO];
			}
		}
	}
	while (0);
	
	[self pwUnlock];
	
	// clear sensitive info
	bzero( &localRec, sizeof(localRec) );
		
	return result;
}


//------------------------------------------------------------------------------------------------
//	mergeKerberosRecord
//
//	Returns: 0 == success, -1 == fail, -2 == incompatible databases, -3 == db file busy
//
//  Updates the database account records; assumes the header has been updated separately
//------------------------------------------------------------------------------------------------

-(SyncStatus)mergeKerberosRecord:(unsigned char *)inRemoteKerberosRecordData
	recordDataLen:(int)inRemoteKerberosRecordDataLen
	withList:(PWSFKerberosPrincipalList *)inKerbList
	modList:(PWSFKerberosPrincipalList *)inOutModKerbList
	timeSkew:(long)inTimeSkew
	accepted:(BOOL *)outAccepted
{
	PWSFKerberosPrincipal *localKerbRec = NULL;
	PWSFKerberosPrincipal *remoteKerbRec = NULL;
	
	if ( outAccepted != NULL )
		*outAccepted = NO;
	
	if ( inRemoteKerberosRecordData == NULL || inKerbList == NULL || inOutModKerbList == NULL )
		return kSyncStatusFail;
	
	remoteKerbRec = inOutModKerbList->ReadPrincipalFromData(inRemoteKerberosRecordData, inRemoteKerberosRecordDataLen);
	if ( remoteKerbRec == NULL )
		return kSyncStatusFail;
	
	localKerbRec = inKerbList->GetPrincipalByName( remoteKerbRec->GetName() );
	if ( localKerbRec == NULL )
	{
		// not in the database yet (or not in the cache)
		*outAccepted = YES;
		return kSyncStatusNoErr;
	}
	
	// Check the modate for a winner.
	// If the local kerb record exists and has a moddate at least as recent as the
	// remote record, then remove the record from the list to update.
	if ((localKerbRec != NULL) && (remoteKerbRec->GetRecordModDate() <= localKerbRec->GetRecordModDate()))
	{
		delete remoteKerbRec;
		remoteKerbRec = NULL;
	}
	else
	{
		*outAccepted = YES;
	}
		
	return kSyncStatusNoErr;
}


#pragma mark -
#pragma mark POLICY TESTING
#pragma mark -

//------------------------------------------------------------------------------------------------
//	disableStatus
//
//	Returns: kAuthOK, kAuthUserDisabled, kAuthPasswordExpired
//  <outReasonCode> is only valid if the return value is kAuthUserDisabled.
//------------------------------------------------------------------------------------------------

-(int)disableStatus:(PWFileEntry *)inOutPasswordRec changed:(BOOL *)outChanged reason:(PWDisableReasonCode *)outReasonCode
{
	PWAccessFeatures *access;
    int result = kAuthOK;
	PWDisableReasonCode reason;
	
    if ( inOutPasswordRec == NULL || outChanged == NULL )
        return kAuthFail;
        
    *outChanged = false;
	if ( outReasonCode != NULL )
		*outReasonCode = kPWDisabledNotSet;
	access = &inOutPasswordRec->access;
	
    // do not disable administrators
    if ( access->isAdminUser || inOutPasswordRec->extraAccess.isComputerAccount )
        return kAuthOK;
    
	if ( inOutPasswordRec->access.isDisabled )
		return kAuthUserDisabled;
	
	result = pwsf_TestDisabledStatusWithReasonCode( access, &mPWFileHeader.access, (struct tm *)&inOutPasswordRec->creationDate, (struct tm *)&inOutPasswordRec->lastLogin, &inOutPasswordRec->failedLoginAttempts, &reason );
	
	// update the user record
	if ( result == kAuthUserDisabled )
	{
		BOOL markDisabled = YES;
		
		// Note: maxMinutesOfNonUse is special
		// If a user logs in in the nick-of-time on a replica, then synchronizing should un-disable the
		// account. Therefore, we do not want to toggle the disabled bit.
		if ( access->maxMinutesOfNonUse > 0 )
		{
			if ( LoginTimeIsStale( &inOutPasswordRec->lastLogin, access->maxMinutesOfNonUse ) )
				markDisabled = NO;
		}
		else
		if ( mPWFileHeader.access.maxMinutesOfNonUse > 0 &&
			 LoginTimeIsStale( &inOutPasswordRec->lastLogin, mPWFileHeader.access.maxMinutesOfNonUse ) )
		{
			markDisabled = NO;
		}		
		
		if ( markDisabled )
		{
			inOutPasswordRec->access.isDisabled = true;
			*outChanged = true;
		}
		
		if ( outReasonCode != NULL )
			*outReasonCode = reason;
	}
	
    return result;
}


//------------------------------------------------------------------------------------------------
//	ChangePasswordStatus
//
//	Returns: kAuthOK, kAuthPasswordNeedsChange, kAuthPasswordExpired
//------------------------------------------------------------------------------------------------

-(int)changePasswordStatus:(PWFileEntry *)inPasswordRec
{
	BOOL needsChange = NO;
	
	if ( inPasswordRec->access.isAdminUser || inPasswordRec->extraAccess.isComputerAccount )
		return kAuthOK;
	
	if ( inPasswordRec->access.newPasswordRequired )
	{
		needsChange = YES;
	}
	else
	{
		// usingExpirationDate
		if ( inPasswordRec->access.usingExpirationDate )
		{
			if ( TimeIsStale( &inPasswordRec->access.expirationDateGMT ) )
				needsChange = YES;
		}
		else
		if ( mPWFileHeader.access.usingExpirationDate && TimeIsStale( &mPWFileHeader.access.expirationDateGMT ) )
		{
			needsChange = YES;
		}
		
		// maxMinutesUntilChangePassword
		if ( inPasswordRec->access.maxMinutesUntilChangePassword > 0 )
		{
			if ( LoginTimeIsStale( &inPasswordRec->modDateOfPassword, inPasswordRec->access.maxMinutesUntilChangePassword ) )
				needsChange = YES;
		}
		else
		if ( mPWFileHeader.access.maxMinutesUntilChangePassword > 0 && LoginTimeIsStale( &inPasswordRec->modDateOfPassword, mPWFileHeader.access.maxMinutesUntilChangePassword ) )
		{
			needsChange = YES;
		}
	}
	
	if ( needsChange )
    {
        if ( inPasswordRec->access.canModifyPasswordforSelf || inPasswordRec->access.isAdminUser )
            return kAuthPasswordNeedsChange;
        else
        	return kAuthPasswordExpired;
    }
    
    return kAuthOK;
}


//------------------------------------------------------------------------------------------------
//	requiredCharacterStatus
//
//	Returns: enum of Reposonse Codes (CAuthFileCPP.h)
//------------------------------------------------------------------------------------------------

-(int)requiredCharacterStatus:(PWFileEntry *)inPasswordRec forPassword:(const char *)inPassword
{
	return pwsf_RequiredCharacterStatusExtra( &(inPasswordRec->access), &(mPWFileHeader.access), inPasswordRec->usernameStr, inPassword, &(inPasswordRec->extraAccess) );
}


//------------------------------------------------------------------------------------------------
//	passwordHistoryStatus
//
//	Returns: kAuthOK, kAuthPasswordNeedsChange (recycled password)
//------------------------------------------------------------------------------------------------

-(int)passwordHistoryStatus:(PWFileEntry *)inPasswordRec password:(const char *)inPassword
{
	int historyCount = 0;
	int result;
	
    if ( inPasswordRec->access.usingHistory || mPWFileHeader.access.usingHistory )
	{
		// At a minimum, do not allow the same password again
		if ( strcmp( inPassword, inPasswordRec->passwordStr ) == 0 )
		{
			return kAuthPasswordNeedsChange;
		}
		
		if ( inPasswordRec->access.historyCount > 0 )
			historyCount = inPasswordRec->access.historyCount;
		else
		if ( GlobalHistoryCount(mPWFileHeader.access) > 0 )
		{
			historyCount = GlobalHistoryCount(mPWFileHeader.access);
		}
		
		if ( historyCount > 0 )
		{
			result = [self checkHistory:inPasswordRec count:historyCount password:inPassword];
			if ( result == -1 )
				return kAuthPasswordNeedsChange;
		}
	}
	
    return kAuthOK;
}


//------------------------------------------------------------------------------------------------
//	ReenableStatus
//
//	Returns: bool
//------------------------------------------------------------------------------------------------

-(int)reenableStatus:(PWFileEntry *)inPasswordRec enableMinutes:(unsigned long)inGlobalReenableMinutes
{
	unsigned long inMaxMinutes = inPasswordRec->extraAccess.minutesUntilFailedLoginReset;
	
	if ( inMaxMinutes == 0 )
		inMaxMinutes = inGlobalReenableMinutes;
	if ( inMaxMinutes == 0 )
		return 0;
	
	if ( inPasswordRec->access.isDisabled &&
		 inPasswordRec->disableReason == kPWDisabledTooManyFailedLogins &&
		 LoginTimeIsStale( &(inPasswordRec->modificationDate), inMaxMinutes ) )
	{
		return 1;
	}
	
	return 0;
}


#pragma mark -
#pragma mark HISTORY ACCESSORS
#pragma mark -

//------------------------------------------------------------------------------------------------
//	checkHistory
//
//	Returns: -1 for reused password, or 0 for good password
//	Checks to see if <inPassword> is in the password history
//------------------------------------------------------------------------------------------------

-(int)checkHistory:(PWFileEntry *)inPasswordRec count:(int)inMaxHistory password:(const char *)inPassword
{
	char historyData[16*sizeof(HMAC_MD5_STATE)];
	int err;
	char *tptr;
	int index;
	unsigned long hashLen;
	unsigned char passwordHash[sizeof(HMAC_MD5_STATE)];
	
	if ( inMaxHistory > 16 )
		inMaxHistory = 16;
	
	err = [self getHistory:inPasswordRec data:historyData];
	
	// if there is no history, then any password is good
	if ( err != 0 )
		return 0;
	
	// get the CRAM-MD5 hash of the password
	pwsf_getHashCramMD5( (unsigned char *)inPassword, strlen(inPassword), passwordHash, &hashLen );
	
	tptr = historyData;
	for ( index = 0; index < inMaxHistory; index++ )
	{
		if ( memcmp( passwordHash, tptr, sizeof(HMAC_MD5_STATE) ) == 0 )
			return -1;
		
		tptr += sizeof(HMAC_MD5_STATE);
	}
	
	return 0;
}


//------------------------------------------------------------------------------------------------
//	putInHistory
//
//	Returns: -1, errno, or 0 for success
//------------------------------------------------------------------------------------------------

-(int)putInHistory:(PWFileEntry *)inPasswordRec passwordHash:(const char *)inPasswordHash
{
	char historyData[16*sizeof(HMAC_MD5_STATE)];
	int err;
	int historyCount = 0;
	int idx;
	
	if ( inPasswordRec->access.usingHistory )
		historyCount = inPasswordRec->access.historyCount;
	else
	if ( mPWFileHeader.access.usingHistory )
		historyCount = GlobalHistoryCount(mPWFileHeader.access);
	
	if ( historyCount == 0 )
		return 0;
	
	// Does the user have a history already?
	err = [self getHistory:inPasswordRec data:historyData];
	
	// if there is no history, then add a record
	if ( err != 0 )
	{
		err = [self addHistory:inPasswordRec];
		
		// if we couldn't add a history record, we're out of luck
		if ( err != 0 )
			return -1;
		
		bzero( historyData, sizeof(historyData) );
	}
	
	// dump the oldest entry
	// memcpy does not support forward overlapping
	for ( idx = 15; idx >= 1; idx-- )
		memcpy( historyData + idx*sizeof(HMAC_MD5_STATE),
				historyData + (idx-1)*sizeof(HMAC_MD5_STATE),
				sizeof(HMAC_MD5_STATE) );
	
	// add the new one
	memcpy( historyData, inPasswordHash, sizeof(HMAC_MD5_STATE) );
	
	// only store the maximum history count
	// add +1 to the count to include the current password
	// (if usingHistory==true and historyCount==0, that's really a history of 1)
	if ( historyCount < 15 )
		bzero( historyData + (historyCount+1)*sizeof(HMAC_MD5_STATE),
				sizeof(historyData) - (historyCount+1)*sizeof(HMAC_MD5_STATE) );
	
	// write to file
	err = [self saveHistory:inPasswordRec data:historyData];
	
	return err;
}


//------------------------------------------------------------------------------------------------
//	getHistory
//
//	Returns: -1, errno, or 0 for success
//	outHistoryData		<- 		The entire block of history data for the given user ID
//------------------------------------------------------------------------------------------------

-(int)getHistory:(PWFileEntry *)inPasswordRec data:(char *)outHistoryData
{
	char historyPath[1024] = {0,};
	char buff[kPWUserIDSize + 16*sizeof(HMAC_MD5_STATE)];
	off_t offset = 0;
	off_t byteCount;
	FILE *fp;
	int err = -1;
	PWFileEntry *dbEntry;
	
	if ( inPasswordRec == NULL || outHistoryData == NULL )
		return -1;
	
	sprintf( historyPath, "%s/%s", mDirPathStr, kPWHistoryFileName );
	fp = fopen( historyPath, "r" );
	if ( fp == NULL ) {
		err = errno;
		if ( err == 0 )
			err = -1;
		return err;
	}
	
	offset = [self historySlotToOffset:inPasswordRec->slot];
	
	byteCount = pread( fileno(fp), buff, sizeof(buff), offset );
	
	dbEntry = (PWFileEntry *)buff;
	
	if ( inPasswordRec->time == dbEntry->time &&
		 inPasswordRec->rnd == dbEntry->rnd &&
		 inPasswordRec->sequenceNumber == dbEntry->sequenceNumber &&
		 inPasswordRec->slot == dbEntry->slot )
	{
		// copy it out
		memcpy( outHistoryData, buff + kPWUserIDSize, sizeof(buff) - kPWUserIDSize );
				
		// zero our copy
		bzero( buff, sizeof(buff) );
		
		err = 0;
	}
		
	fclose( fp );
	
	return err;
}


//------------------------------------------------------------------------------------------------
//	addHistory
//
//	Returns: -1, errno, or 0 for success
//	Appends a new record to the appropriate file for the given user ID
//------------------------------------------------------------------------------------------------

-(int)addHistory:(PWFileEntry *)inPasswordRec
{
	char historyPath[1024] = {0,};
	char buff[16*sizeof(HMAC_MD5_STATE)];
	int writeCount;
	FILE *fp;
	int err = -1;
	off_t offset;
	struct stat sb;
	
	if ( inPasswordRec == NULL )
		return -1;
	
	offset = [self historySlotToOffset:inPasswordRec->slot];
	
    // validate
    sprintf( historyPath, "%s/%s", mDirPathStr, kPWHistoryFileName );	
	fp = fopen( historyPath, "w+" );
	if ( fp == NULL ) {
		err = errno;
		if ( err == 0 )
			err = -1;
		return err;
	}
	
	err = lstat( historyPath, &sb );
	if ( err == 0 )
	{
		// is the file big enough?
		if ( (int64_t)(offset + kPWUserIDSize + sizeof(buff)) > sb.st_size )
		{
			long amountToWrite = offset + kPWUserIDSize + sizeof(buff) - sb.st_size;
			char *zeroBuff = (char *) calloc( 1, amountToWrite );
			
			err = fseek( fp, 0, SEEK_END );
			if ( err == 0 )
			{
				writeCount = fwrite( zeroBuff, amountToWrite, 1, fp );
				if ( writeCount != 1 )
					err = -1;
			}
			
			free( zeroBuff );
		}
		
		if ( err == 0 )
		{
			err = fseek( fp, offset, SEEK_SET );
			if ( err == 0 )
			{
				writeCount = fwrite( inPasswordRec, kPWUserIDSize, 1, fp );
				if ( writeCount != 1 )
					err = -1;
			}
		}
	}
		
	fclose( fp );
	
	return err;
}


//------------------------------------------------------------------------------------------------
//	saveHistory
//
//	Returns: -1, errno, or 0 for success
//	inOutHistoryData	<->		The entire 8K block of history data for the given user ID
//								On exit, the data is encrypted
//------------------------------------------------------------------------------------------------

-(int)saveHistory:(PWFileEntry *)inPasswordRec data:(char *)inOutHistoryData
{
	char historyPath[1024] = {0,};
	off_t offset = 0;
	off_t byteCount;
	FILE *fp;
	int err = -1;
	const long sizeOfHistory = 16*sizeof(HMAC_MD5_STATE);
	
	if ( inPasswordRec == NULL || inOutHistoryData == NULL )
		return -1;
	
	sprintf( historyPath, "%s/%s", mDirPathStr, kPWHistoryFileName );	
	fp = fopen( historyPath, "r+" );
	if ( fp == NULL ) {
		err = errno;
		if ( err == 0 )
			err = -1;
		return err;
	}
	
	offset = [self historySlotToOffset:inPasswordRec->slot];
	
	byteCount = pwrite( fileno(fp), inOutHistoryData, sizeOfHistory, offset + kPWUserIDSize );
	if ( byteCount == sizeOfHistory )
		err = 0;
	
	fclose( fp );
	
	return err;
}


//------------------------------------------------------------------------------------------------
//	historySlotToOffset
//
//	Returns: the position in the history database file for the slot
//------------------------------------------------------------------------------------------------

-(unsigned long)historySlotToOffset:(unsigned long)inSlotNumber
{
	// each slot is:
	// 16 byte userID
	// 512 bytes of CRAM-MD5 hashes (16 hashes * 32 bytes each)
	
	return ( (inSlotNumber-1) * ( kPWUserIDSize + 16*sizeof(HMAC_MD5_STATE)) );
}


#pragma mark -
#pragma mark MISC
#pragma mark -

//------------------------------------------------------------------------------------------------
-(SyncPriority)syncPriority
{
	return mSyncPriority;
}


//------------------------------------------------------------------------------------------------
-(void)setShouldSyncNow:(BOOL)onOff
{
	if ( onOff == YES )
	{
		if ( mSyncPriority == kSyncPriorityNormal )
			mSyncPriority = kSyncPriorityDirty;
	}
	else
	{
		mSyncPriority = kSyncPriorityNormal;
	}
}


//------------------------------------------------------------------------------------------------
-(void)setForceSync:(BOOL)onOff
{
	if ( onOff == YES ) 
	{
		mSyncPriority = kSyncPriorityForce;
	}
	else
	{
		if ( mSyncPriority == kSyncPriorityForce )
			mSyncPriority = kSyncPriorityDirty;
	}
}


//------------------------------------------------------------------------------------------------
-(void)setTestSpillBucket:(BOOL)onOff
{
	mTestSpillBucket = onOff;
}

//------------------------------------------------------------------------------------------------
-(uint32_t)kerberosCacheLimit
{
	return mKerberosCacheLimit;
}

//------------------------------------------------------------------------------------------------
-(void)setKerberosCacheLimit:(uint32_t)inLimit
{
	mKerberosCacheLimit = inLimit;
}


@end