/*
* 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>
#include <pthread.h>
extern "C" {
#import <stdio.h>
#import <string.h>
#import <sys/fcntl.h>
#import <sys/stat.h>
#import <syslog.h>
#import <unistd.h>
#import <time.h>
#import <LDAP/ldap.h>
#import "AuthDBFile.h"
#import "PSUtilitiesDefs.h"
#import "SASLCode.h"
#import "SMBAuth.h"
#import "ReplicaFileDefs.h"
#import "KerberosInterface.h"
#import "AuditUtils.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;
if( pthread_mutex_init(&mLocksLock, NULL) != 0 )
return nil;
mPWLockCount = 0;
mPWHdrLastMod = 0;
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;
if( pthread_mutex_init(&mLocksLock, NULL) != 0 )
return nil;
mPWLockCount = 0;
mPWHdrLastMod = 0;
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];
}
-(void)dealloc
{
[self freeRSAKey];
[self closePasswordFile];
[self closeFreeListFile];
if ( mFilePathStr != NULL )
free( mFilePathStr );
if ( mDirPathStr != NULL )
free( mDirPathStr );
if ( mSearchBase != NULL )
free( mSearchBase );
pthread_mutex_destroy(&mLocksLock);
[mOverflow release];
[super dealloc];
}
-free
{
[self release];
return 0;
}
-(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 ( mPWFileNO != -1 )
{
if ( mPWFileHeader.signature != kPWFileSignature ||
mPWFileHeader.version != kPWFileVersion ||
sb.st_size != (off_t)(sizeof(mPWFileHeader) + (off_t)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
// flags duplicate that of fopen( "w+" )
mPWFileNO = open( filePathStr, O_RDWR | O_CREAT | O_TRUNC );
if ( mPWFileNO >= 0 )
{
err = fchmod( mPWFileNO, 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 = (UInt32)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) );
if( [self pwLock:kOneSecondLockInterval] == YES ) {
for ( i = kPWFileInitialSlots; i > 0; i-- )
{
writeCount = pwrite( mPWFileNO, &anEntry, sizeof(PWFileEntry), sizeof(PWFileHeader) + ((i-1)*sizeof(PWFileEntry)) );
if ( writeCount != sizeof(PWFileEntry) )
{
err = -1;
break;
}
}
[self pwUnlock];
} else {
err = -1;
}
}
if ( err == 0 )
[self validateFiles];
else
unlink( filePathStr );
}
else
{
if ( errno )
err = errno;
}
return err;
}
/* Convert fopen(3) flags to open(2) flags. From the internal libc function */
static int
__sflags(const char *mode, int *optr)
{
int m, o;
switch (*mode++) {
case 'r': /* open for reading */
m = O_RDONLY;
o = 0;
break;
case 'w': /* open for writing */
m = O_WRONLY;
o = O_CREAT | O_TRUNC;
break;
case 'a': /* open for appending */
m = O_WRONLY;
o = O_CREAT | O_APPEND;
break;
default: /* illegal mode */
errno = EINVAL;
return (0);
}
/* [rwa]\+ or [rwa]b\+ means read and write */
if (*mode == 'b')
mode++;
if (*mode == '+') {
m = O_RDWR;
mode++;
if (*mode == 'b')
mode++;
}
if (*mode == 'x')
o |= O_EXCL;
*optr = m | o;
return (0);
}
//----------------------------------------------------------------------------------------------------
// 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 ( mPWFileNO != -1 && strcmp( mode, mPWFilePermission ) == 0 )
{
return err;
}
else
{
[self closePasswordFile];
int flags = 0;
__sflags(mode, &flags);
mPWFileNO = open( mFilePathStr, flags );
// handle read-only file system
if ( mPWFileNO < 0 && errno == EROFS )
{
mReadOnlyFileSystem = YES;
mPWFileNO = open( mFilePathStr, O_RDONLY );
}
if ( mPWFileNO >= 0 )
{
strcpy( mPWFilePermission, mReadOnlyFileSystem ? "r" : mode );
}
else
{
err = errno;
if ( err == 0 )
err = -1;
}
}
return err;
}
-(void)closePasswordFile
{
[self pwWait];
if ( mPWFileNO >= 0 ) {
if ( mDBFileLocked )
[self pwUnlock];
close( mPWFileNO );
mPWLockCount = 0;
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
{
[self closePasswordFile];
// force the rsa key to be reloaded
[self freeRSAKey];
}
-(void)pwLock
{
int tries = 3;
int result;
if ( mPWFileNO >= 0 )
{
if( [self pwLockLock:kOneSecondLockInterval] == YES ) {
while ( (result = flock( mPWFileNO, LOCK_EX | LOCK_NB )) == -1 && tries-- > 0 )
usleep( 25000 );
if ( result == 0 ) {
mPWLockCount++;
mDBFileLocked = YES;
}
[self pwLockUnlock];
} else {
syslog(LOG_ERR, "PasswordService unable to obtain lock");
}
}
}
//----------------------------------------------------------------------------------------------------
// pwLock
//
// Returns: YES if the lock is obtained.
//----------------------------------------------------------------------------------------------------
-(BOOL)pwLock:(unsigned long)inMillisecondsToWait
{
const useconds_t millisecondsPerTry = 25;
long tries = inMillisecondsToWait / millisecondsPerTry;
BOOL locked = NO;
if ( mPWFileNO < 0 )
{
mDBFileLocked = NO;
[self openPasswordFile:"r+"];
}
if ( mPWFileNO >= 0 )
{
if ( tries <= 0 )
tries = 1;
while ( tries-- > 0 )
{
if( [self pwLockLock:inMillisecondsToWait] == YES )
{
if ( (mPWLockCount > 0) || (flock( mPWFileNO, LOCK_EX | LOCK_NB ) == 0) )
{
locked = YES;
mPWLockCount++;
[self pwLockUnlock];
break;
}
[self pwLockUnlock];
} else {
syslog(LOG_ERR, "PasswordService unable to obtain lock");
}
usleep( millisecondsPerTry * 1000 );
}
}
mDBFileLocked = locked;
return locked;
}
-(void)pwUnlock
{
if ( mPWFileNO >= 0 ) {
if( [self pwLockLock:kOneSecondLockInterval] == YES ) {
mPWLockCount--;
if( mPWLockCount <= 0 ) {
mPWLockCount = 0;
flock( mPWFileNO, LOCK_UN );
mDBFileLocked = NO;
}
[self pwLockUnlock];
} else {
// new failure condition with no way to report it...
}
} else {
mDBFileLocked = NO;
}
}
-(BOOL)pwLockLock:(unsigned long)inMillisecondsToWait
{
const useconds_t millisecondsPerTry = 25;
long tries = inMillisecondsToWait / millisecondsPerTry;
BOOL locked = NO;
if ( tries <= 0 )
tries = 1;
while ( tries-- > 0 )
{
if ( pthread_mutex_trylock(&mLocksLock) == 0 )
{
locked = YES;
break;
}
usleep( millisecondsPerTry * 1000 );
}
return locked;
}
-(void)pwLockUnlock
{
pthread_mutex_unlock(&mLocksLock);
}
-(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;
struct stat sb;
if ( outHeader == NULL )
return -1;
if ( inCanUseCachedCopy && mGotHeader )
{
memcpy( outHeader, &mPWFileHeader, sizeof(PWFileHeader) );
return 0;
}
memset(&sb, 0, sizeof(sb));
if( lstat(mFilePathStr, &sb) == 0 ) {
if( difftime(sb.st_mtimespec.tv_sec, mPWHdrLastMod) < 0.0 ) {
memcpy( outHeader, &mPWFileHeader, sizeof(PWFileHeader) );
return 0;
}
}
[self pwWait];
err = [self openPasswordFile:mReadOnlyFileSystem ? "r" : "r+"];
if ( err == 0 && (mPWFileNO >= 0) )
{
if( [self pwLock:kOneSecondLockInterval] == YES ) {
readCount = pread( mPWFileNO, outHeader, sizeof(PWFileHeader), 0 );
[self pwUnlock];
pwsf_EndianAdjustPWFileHeader( outHeader, 1 );
if ( outHeader->signature == kPWFileSignature )
{
// adopt the new header data
memcpy( &mPWFileHeader, outHeader, sizeof(PWFileHeader) );
mPWHdrLastMod = sb.st_mtimespec.tv_sec;
}
else
{
err = -2;
// bad news, try to recover
if ( mGotHeader && mPWFileHeader.signature == kPWFileSignature )
{
err = -3;
memcpy( outHeader, &mPWFileHeader, sizeof(PWFileHeader) );
saveAfterReleasingSemaphore = YES;
}
}
mGotHeader = YES;
} else {
err = -4;
}
}
[self pwSignal];
if ( saveAfterReleasingSemaphore )
[self setHeader:&mPWFileHeader];
return err;
}
//----------------------------------------------------------------------------------------------------
// setHeader
//
// Returns: 0=success, -1=fail
//----------------------------------------------------------------------------------------------------
-(int)setHeader:(const PWFileHeader *)inHeader
{
int err = -1;
ssize_t writeCount = 0;
if ( inHeader == NULL )
return -1;
if ( inHeader->signature != kPWFileSignature )
return -1;
if ( mReadOnlyFileSystem )
return -1;
[self pwWait];
err = [self openPasswordFile:"r+"];
if ( err == 0 && (mPWFileNO >= 0) )
{
// adopt the new header data
if ( inHeader != &mPWFileHeader )
memcpy( &mPWFileHeader, inHeader, sizeof(PWFileHeader) );
if( [self pwLock:kOneSecondLockInterval] == YES ) {
// write to disk
#if TARGET_RT_LITTLE_ENDIAN
PWFileHeader diskHeader = mPWFileHeader;
pwsf_EndianAdjustPWFileHeader( &diskHeader, 0 );
writeCount = pwrite( mPWFileNO, &diskHeader, sizeof(PWFileHeader), 0 );
bzero( &diskHeader, sizeof(PWFileHeader) );
#else
writeCount = pwrite( mPWFileNO, &mPWFileHeader, sizeof(PWFileHeader), 0 );
#endif
[self pwUnlock];
}
if ( writeCount != sizeof(PWFileHeader) )
{
err = -1;
}
}
[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;
uint32_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;
} else {
syslog(LOG_ERR, "PasswordService loadRSAkey unable to get header");
}
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;
if( [self pwLock:kOneSecondLockInterval] != YES )
return -1;
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;
if( [self pwLock:kOneSecondLockInterval] != YES )
return -1;
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:(uint32_t)inNumSlots nextAvailableSlot:(uint32_t *)outSlot
{
int err;
ssize_t writeCount;
if ( mReadOnlyFileSystem )
return -1;
[self pwWait];
err = [self openPasswordFile:"r+"];
if ( err == 0 && mPWFileNO >= 0 )
{
struct stat sb;
// write blank space
// fstat since pwrite doesn't take the equivalent of SEEK_END
err = fstat(mPWFileNO, &sb);
if ( err == 0 )
{
PWFileEntry anEntry;
unsigned long i;
bzero( &anEntry, sizeof(PWFileEntry) );
if( [self pwLock:kOneSecondLockInterval] == YES ) {
for ( i = inNumSlots; i > 0; i-- )
{
writeCount = pwrite( mPWFileNO, &anEntry, sizeof(PWFileEntry), sb.st_size + ((i-1) * sizeof(PWFileEntry)) );
if ( writeCount != (ssize_t)sizeof(PWFileEntry) )
{
err = -1;
break;
}
}
[self pwUnlock];
} else {
err = -1;
}
}
if( err == 0 )
{
// update header
mPWFileHeader.numberOfSlotsCurrentlyInFile += inNumSlots;
if ( outSlot != NULL )
{
mPWFileHeader.deepestSlotUsed++;
mPWFileHeader.deepestSlotUsedByThisServer = mPWFileHeader.deepestSlotUsed;
*outSlot = mPWFileHeader.deepestSlotUsed;
}
err = [self setHeader:&mPWFileHeader];
} else {
// A write above may have failed, leaving us with an
// improperly sized database. We'll try to truncate the file
// back to the original size.
if( [self pwLock:kOneSecondLockInterval] == YES ) {
off_t correctsize = sizeof(PWFileHeader) + (mPWFileHeader.numberOfSlotsCurrentlyInFile * sizeof(PWFileEntry));
if( ftruncate(mPWFileNO, correctsize) ) {
syslog(LOG_ALERT, "Unable to expand %s, and unable to truncate back to original size. Database may be corrupt.", kPWFilePath);
}
}
}
}
[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)
if( BN_rand(nonce, 256, 0, 0) != 1 ) {
BN_clear_free(nonce);
return false;
}
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.
//----------------------------------------------------------------------------------------------------
-(uint32_t)nextSlot
{
uint32_t 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(slot), SEEK_END );
if ( err == 0 )
{
curpos = ftell( mFreeListFile );
readCount = fread( &slot, sizeof(slot), 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;
uint32_t publicKeyLen = 0;
unsigned char *privateKey = NULL;
uint32_t privateKeyLen = 0;
char bitCountStr[256] = {0,};
char tempFileStr[256] = {0,};
char publicKeyFileStr[256] = {0,};
struct stat sb = {0};
const char *argv[] = { "/usr/bin/ssh-keygen",
"-t", "rsa1",
"-b", (const char *)bitCountStr,
"-f", (const char *)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", (char * const *)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 = (uint32_t)sb.st_size;
privateKey = (unsigned char *) malloc( privateKeyLen + 1 );
fread( (char*)privateKey, privateKeyLen, 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 = (uint32_t)sb.st_size;
publicKey = (unsigned char *) malloc( publicKeyLen + 1 );
fread( publicKey, publicKeyLen, 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:(uint32_t)publicKeyLen
privateKey:(unsigned char *)privateKey
privateKeyLen:(uint32_t)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" );
if( [self pwLock:kOneSecondLockInterval] != YES )
return -1;
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;
if( [self pwLock:kOneSecondLockInterval] != YES )
return -1;
// 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 = (UInt32)pwsf_getTimeForRef();
passwordRec->rnd = (UInt32)pwsf_getRandom();
passwordRec->sequenceNumber = ++mPWFileHeader.sequenceNumber;
passwordRec->slot = [self nextSlot];
pwsf_getGMTime( &passwordRec->creationDate );
memcpy( &passwordRec->lastLogin, &passwordRec->creationDate, sizeof(passwordRec->lastLogin) );
memcpy( &passwordRec->modDateOfPassword, &passwordRec->creationDate, sizeof(passwordRec->modDateOfPassword) );
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:(uint32_t)slot obfuscate:(BOOL)obfuscate
{
return [self addPassword:passwordRec atSlot:slot obfuscate:obfuscate setModDate:YES];
}
-(int)addPassword:(PWFileEntry *)passwordRec atSlot:(uint32_t)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:(uint32_t)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;
if( [self pwLock:kOneSecondLockInterval] != YES )
return -1;
// 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 = (UInt32)pwsf_getTimeForRef();
passwordRec->rnd = (UInt32)pwsf_getRandom();
passwordRec->sequenceNumber = ++mPWFileHeader.sequenceNumber;
passwordRec->slot = [self nextSlot];
pwsf_getGMTime( &passwordRec->creationDate );
memcpy( &passwordRec->lastLogin, &passwordRec->creationDate, sizeof(passwordRec->lastLogin) );
memcpy( &passwordRec->modDateOfPassword, &passwordRec->creationDate, sizeof(passwordRec->modDateOfPassword) );
// 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:(uint32_t)slot putItHere:(PWFileEntry *)passRec
{
return [self getPasswordRec:slot putItHere:passRec unObfuscate:YES];
}
-(int)getPasswordRec:(uint32_t)slot putItHere:(PWFileEntry *)passRec unObfuscate:(BOOL)unObfuscate
{
off_t offset;
int err = -1;
ssize_t readCount = 0;
if ( slot > 0 )
{
[self pwWait];
err = [self openPasswordFile:mReadOnlyFileSystem ? "r" : "r+"];
if ( err == 0 && (mPWFileNO >= 0) )
{
offset = pwsf_slotToOffset( slot );
if( [self pwLock:kOneSecondLockInterval] == YES ) {
readCount = pread( mPWFileNO, passRec, sizeof(PWFileEntry), offset );
[self pwUnlock];
}
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 = BSDTimeStructCopy_timegm( &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;
uint32_t slot = passwordRec->slot;
size_t 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( &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(slot), 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:(uint32_t)slot
{
return [self setPassword:passwordRec atSlot:slot obfuscate:YES setModDate:YES];
}
-(int)setPassword:(PWFileEntry *)passwordRec
atSlot:(uint32_t)slot
obfuscate:(BOOL)obfuscate
setModDate:(BOOL)setModDate
{
off_t offset;
int err = -1;
ssize_t writeCount = 0;
size_t encodeLen;
if ( mReadOnlyFileSystem )
return -1;
if ( slot > 0 )
{
if ( (unsigned long)slot > mPWFileHeader.numberOfSlotsCurrentlyInFile )
return -1;
if ( setModDate )
pwsf_getGMTime( &passwordRec->modificationDate );
[self pwWait];
err = [self openPasswordFile:"r+"];
if ( err == 0 && mPWFileNO >= 0 )
{
offset = pwsf_slotToOffset( slot );
//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 );
if( [self pwLock:kOneSecondLockInterval] == YES ) {
writeCount = pwrite( mPWFileNO, passwordRec, sizeof(PWFileEntry), offset );
[self pwUnlock];
}
pwsf_EndianAdjustPWFileEntry( passwordRec, 1 );
if ( obfuscate )
pwsf_DESDecode(passwordRec->passwordStr, encodeLen);
if ( writeCount != (ssize_t)sizeof(PWFileEntry) )
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:(uint32_t)slot
{
off_t offset;
int err = -1;
ssize_t writeCount = 0;
size_t encodeLen;
if ( mReadOnlyFileSystem )
return -1;
if ( slot > 0 )
{
if ( (unsigned long)slot > mPWFileHeader.numberOfSlotsCurrentlyInFile )
return -1;
pwsf_getGMTime( &passwordRec->modificationDate );
[self pwWait];
err = [self openPasswordFile:"r+"];
if ( err == 0 && mPWFileNO >= 0 )
{
offset = pwsf_slotToOffset( slot );
//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 );
if( [self pwLock:kOneSecondLockInterval] == YES ) {
writeCount = pwrite( mPWFileNO, passwordRec, sizeof(PWFileEntry), offset );
[self pwUnlock];
}
pwsf_EndianAdjustPWFileEntry( passwordRec, 1 );
if ( writeCount != (ssize_t)sizeof(PWFileEntry) )
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;
uint32_t 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];
const char *argv[] = {"/usr/bin/ldapsearch", "-LLL", "-x", "-z", "1", "-b", "", "-s", "base", "namingContexts", NULL};
int result = pwsf_LaunchTaskWithIO("/usr/bin/ldapsearch", (char * const *)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];
const char *argv[] = { "/usr/bin/ldapsearch", "-LLL", "-x", "-z", "1", "-b", (const char *)searchBase,
(const char *)filter, "authAuthority", NULL };
snprintf( filter, sizeof(filter), "(|(uid=%s)(cn=%s))", inUID, inUID );
result = pwsf_LaunchTaskWithIO("/usr/bin/ldapsearch", (char * const *)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;
uint32_t 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;
size_t 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 = (unsigned long)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;
size_t 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 = BSDTimeStructCopy_timegm( &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 = BSDTimeStructCopy_timegm( &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;
size_t readCount;
off_t 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;
size_t 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 < (off_t)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 - (UInt32)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 = BSDTimeStructCopy_timegm( &localRec.modDateOfPassword );
remoteTime = BSDTimeStructCopy_timegm( &remoteRec.modDateOfPassword ) - inTimeSkew;
if ( inTimeSkew != 0 )
BSDTimeStructCopy_gmtime_r( &remoteTime, &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 = BSDTimeStructCopy_timegm( &localRec.lastLogin );
remoteTime = BSDTimeStructCopy_timegm( &remoteRec.lastLogin );
if ( remoteTime > inTimeSkew )
remoteTime -= inTimeSkew;
if ( inTimeSkew != 0 )
BSDTimeStructCopy_gmtime_r( &remoteTime, &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 = BSDTimeStructCopy_timegm( &localRec.modificationDate );
remoteTime = BSDTimeStructCopy_timegm( &remoteRec.modificationDate ) - inTimeSkew;
if ( inTimeSkew != 0 )
BSDTimeStructCopy_gmtime_r( &remoteTime, &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)++;
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);
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 - (UInt32)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;
BOOL isNew = 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;
}
isNew = 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 = BSDTimeStructCopy_timegm( &localRec.modDateOfPassword );
remoteTime = BSDTimeStructCopy_timegm( &inRemoteRecord->modDateOfPassword ) - inTimeSkew;
if ( inTimeSkew != 0 )
BSDTimeStructCopy_gmtime_r( &remoteTime, &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 = BSDTimeStructCopy_timegm( &localRec.lastLogin );
remoteTime = BSDTimeStructCopy_timegm( &inRemoteRecord->lastLogin );
if ( remoteTime > inTimeSkew )
remoteTime -= inTimeSkew;
if ( inTimeSkew != 0 )
BSDTimeStructCopy_gmtime_r( &remoteTime, &inRemoteRecord->lastLogin );
if ( remoteTime > localTime )
{
memcpy( &localRec.lastLogin, &inRemoteRecord->lastLogin, sizeof(BSDTimeStructCopy) );
bNeedsUpdate = YES;
}
// all non-special fields
localTime = BSDTimeStructCopy_timegm( &localRec.modificationDate );
remoteTime = BSDTimeStructCopy_timegm( &inRemoteRecord->modificationDate ) - inTimeSkew;
if ( inTimeSkew != 0 )
BSDTimeStructCopy_gmtime_r( &remoteTime, &inRemoteRecord->modificationDate );
if ( remoteTime > localTime )
{
if ( !isNew && (localRec.access.isDisabled != inRemoteRecord->access.isDisabled) )
{
short event = 0;
char slotid[35] = {0,};
pwsf_passwordRecRefToString(inRemoteRecord, slotid);
if ( inRemoteRecord->access.isDisabled == 0 )
{
event = AUE_enable_user;
} else {
event = AUE_disable_user;
}
if ( audit_disabled_user(NULL, slotid, event) != 0 )
{
result = kSyncStatusFail;
break;
}
}
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, &inOutPasswordRec->creationDate, &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 ) )
{
// TimeIsStale only tells us if we're more recent
// than the expiration date, it doesn't tell us if the
// user has changed their password since then.
// So we check here.
time_t changetime = BSDTimeStructCopy_timegm(&inPasswordRec->modDateOfPassword);
time_t expiretime = BSDTimeStructCopy_timegm(&inPasswordRec->access.expirationDateGMT);
if( difftime(changetime, expiretime) < 0 )
needsChange = YES;
}
}
else
if ( mPWFileHeader.access.usingExpirationDate && TimeIsStale( &mPWFileHeader.access.expirationDateGMT ) )
{
time_t changetime = BSDTimeStructCopy_timegm(&inPasswordRec->modDateOfPassword);
time_t expiretime = BSDTimeStructCopy_timegm(&mPWFileHeader.access.expirationDateGMT);
if( difftime(changetime, expiretime) < 0 )
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) );
}
else
{
// If the user has a history make sure this hash isn't already in the
// history. In theory, there shouldn't be duplicates in the history;
// however because PWS calls kadmin to change the password, which calls
// back to PWS to change the password, duplicates can result. If that
// happens, just return OK. If the user really tries to change their
// password to something in the history, that will get caught way before
// the code gets here.
char* tptr = historyData;
for ( idx = 0; idx < historyCount; idx++ )
{
if ( memcmp( inPasswordHash, tptr, sizeof(HMAC_MD5_STATE) ) == 0 )
return 0;
tptr += sizeof(HMAC_MD5_STATE);
}
}
// 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)];
size_t writeCount;
FILE *fp = NULL;
int err = -1;
off_t offset;
struct stat sb;
if ( inPasswordRec == NULL )
return -1;
offset = [self historySlotToOffset:inPasswordRec->slot];
sprintf( historyPath, "%s/%s", mDirPathStr, kPWHistoryFileName );
// Use open(2) so the file will get created if needed, but not truncated.
int fd = open( historyPath, O_RDWR|O_CREAT );
if (fd >= 0) {
fp = fdopen( fd, "r+" );
}
if ( fp == NULL ) {
err = errno;
if ( err == 0 )
err = -1;
return err;
}
err = stat( historyPath, &sb );
if ( err == 0 )
{
// is the file big enough?
if ( (off_t)(offset + kPWUserIDSize + sizeof(buff)) > sb.st_size )
{
size_t amountToWrite = (size_t)(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 = fseeko( 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
//------------------------------------------------------------------------------------------------
-(off_t)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