/* * 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 #include #include #include #include #include #include #include #include "DSMutexSemaphore.h" #include "CAuthFileBase.h" #include "SASLCode.h" extern "C" { #include "saslutil.h" #if COMPILE_WITH_RSA_LOAD #include "bufaux.h" #include "buffer.h" #include "cipher.h" #include "xmalloc.h" #include "ssh.h" #endif }; #define kFixedDESKey "1POTATO2potato3PotatoFOUR" //#define kFixedDESKey "M&2y(V40" #define kFixedDESChunk 8 #define kMaxWriteSuspendTime 2 // seconds #define kPWUserIDSize 4*sizeof(long) /* Version identification string for identity files. */ #define AUTHFILE_ID_STRING "SSH PRIVATE KEY FILE FORMAT 1.1\n" #define Max(A,B) (((A) > (B)) ? (A):(B)) extern int errno; CAuthFileBase::CAuthFileBase() { this->Init(); } CAuthFileBase::CAuthFileBase( const char *inDBFilePath ) { this->Init(); if ( inDBFilePath != NULL && strlen(inDBFilePath) < sizeof(fFilePath) ) strcpy( fFilePath, inDBFilePath ); } CAuthFileBase::~CAuthFileBase() { this->closePasswordFile(); } void CAuthFileBase::Init(void) { time_t now; pwFile = NULL; freeListFile = NULL; pwFileHeader.signature = 'null'; pwFileValidated = false; pwFilePermission[0] = '\0'; pwFileBasePtr = NULL; pwFileLen = 0; rsaKey = NULL; fWriteSuspended = false; fGotHeader = false; strcpy( fFilePath, kPWFilePath ); // seed random # generator time(&now); srandom((UInt32)now); } int CAuthFileBase::validateFiles(void) { int err; err = this->validatePasswordFile(); this->validateFreeListFile(); return err; } //---------------------------------------------------------------------------------------------------- // validatePasswordFile // // Returns: file errors // // Do internal checks on the database file. // Checks: signature, version, and size. //---------------------------------------------------------------------------------------------------- int CAuthFileBase::validatePasswordFile(void) { int err; struct stat sb; PWFileHeader dbHeader; // validate err = lstat( fFilePath, &sb ); if ( err == 0 ) err = this->getHeader( &dbHeader ); if ( err == 0 ) { if ( pwFile != NULL ) { if ( pwFileHeader.signature != kPWFileSignature || pwFileHeader.version != kPWFileVersion || sb.st_size != sizeof(PWFileHeader) + pwFileHeader.numberOfSlotsCurrentlyInFile * sizeof(PWFileEntry) ) { err = -1; } } else { err = -1; } } if ( err == 0 ) pwFileValidated = true; return err; } int CAuthFileBase::validateFreeListFile(void) { return 0; } int CAuthFileBase::createPasswordFile(void) { int err = -1; size_t writeCount; // make sure the directory exists err = mkdir( kPWDirPath, S_IRWXU ); // if it existed before, double-check the permissions if ( err != 0 && errno == EEXIST ) err = chmod( kPWDirPath, S_IRWXU ); // create new file pwFile = fopen( fFilePath, "w+" ); if ( pwFile != NULL ) { err = chmod( fFilePath, S_IRUSR | S_IWUSR ); if ( err == -1 ) err = errno; // ignore err = 0; // set header initial state bzero( &pwFileHeader, sizeof(PWFileHeader) ); pwFileHeader.signature = kPWFileSignature; pwFileHeader.version = kPWFileVersion; pwFileHeader.sequenceNumber = 0; pwFileHeader.numberOfSlotsCurrentlyInFile = kPWFileInitialSlots; pwFileHeader.deepestSlotUsed = 0; pwFileHeader.deepestSlotUsedByThisServer = 0; pwFileHeader.access.usingHistory = false; pwFileHeader.access.usingExpirationDate = false; pwFileHeader.access.usingHardExpirationDate = false; pwFileHeader.access.requiresAlpha = false; pwFileHeader.access.requiresNumeric = false; pwFileHeader.access.passwordIsHash = false; // do not need to set these if usingExpirationDate and usingHardExpirationDate are false //pwFileHeader.access.expirationDateGMT //pwFileHeader.access.hardExpireDateGMT pwFileHeader.access.maxMinutesUntilChangePassword = 0; pwFileHeader.access.maxMinutesUntilDisabled = 0; pwFileHeader.access.maxMinutesOfNonUse = 0; pwFileHeader.access.maxFailedLoginAttempts = 0; pwFileHeader.access.minChars = 0; pwFileHeader.access.maxChars = 0; // write header writeCount = fwrite( &pwFileHeader, sizeof(PWFileHeader), 1, pwFile ); if ( writeCount != 1 ) { err = -1; } // write blank space if ( err == 0 ) { PWFileEntry anEntry; int i; bzero( &anEntry, sizeof(PWFileEntry) ); for ( i = kPWFileInitialSlots; i > 0; i-- ) { writeCount = fwrite( &anEntry, sizeof(PWFileEntry), 1, pwFile ); if ( writeCount != 1 ) { err = -1; break; } } } this->closePasswordFile(); if ( err != 0 ) remove( fFilePath ); } else { if ( errno ) err = errno; } return err; } //---------------------------------------------------------------------------------------------------- // openPasswordFile // // Returns: file errors // Does not Return: file mapping errors. // // Utility function to open the database. When the database is opened with write-access, // the file is not mapped to keep it immediately up-to-date. For read-only, the file is // mapped by all except getPasswordRec. Access is switched to write-access for each AUTH, // so there is no advantage to mapping. //---------------------------------------------------------------------------------------------------- int CAuthFileBase::openPasswordFile(const char *mode, Boolean map) { int err = 0; if ( pwFile && strcmp( mode, pwFilePermission ) == 0 ) { return err; } else { this->closePasswordFile(); pwFile = fopen( fFilePath, mode ); if ( pwFile ) { strcpy( pwFilePermission, mode ); if ( map && strcmp( mode, "r" ) == 0 ) { err = this->mapPasswordFile(); // do not report mapping errors. all methods can operate unmapped. err = 0; } } else { err = errno; if ( err == 0 ) err = -1; } } return err; } int CAuthFileBase::mapPasswordFile(void) { int err; struct stat sb; int fileNum; // can't map if not open if ( pwFile == NULL ) return -1; // dump the old if ( pwFileBasePtr ) { munmap( pwFileBasePtr, pwFileLen ); pwFileBasePtr = nil; } // get file len pwFileLen = 0; err = lstat( fFilePath, &sb ); if ( err == 0 ) pwFileLen = sb.st_size; // map if ( pwFileLen > 0 ) { fileNum = fileno( pwFile ); pwFileBasePtr = (caddr_t) mmap( 0, pwFileLen, PROT_READ | PROT_WRITE, MAP_FILE, fileNum, 0 ); if ( (long)pwFileBasePtr == -1 ) { err = errno; // errno could get stomped by another thread if ( err == 0 ) err = -1; // let everyone know there is no map pwFileBasePtr = NULL; // do not close the file; go about life unmapped //this->closePasswordFile(); } } return err; } void CAuthFileBase::closePasswordFile(void) { if ( pwFile ) { if ( pwFileBasePtr ) { munmap( pwFileBasePtr, pwFileLen ); pwFileBasePtr = nil; pwFileLen = 0; } fclose( pwFile ); pwFile = nil; } fGotHeader = false; } void CAuthFileBase::closeFreeListFile(void) { if ( freeListFile ) { fclose( freeListFile ); freeListFile = nil; } } void CAuthFileBase::resetPasswordFileState(void) { if ( pwFile ) fflush( pwFile ); pwWait(); closePasswordFile(); pwSignal(); // force the rsa key to be reloaded rsaWait(); RSA_free( rsaKey ); rsaKey = NULL; rsaSignal(); } void CAuthFileBase::carryOn( void ) { } void CAuthFileBase::pwLock(void) { int tries = 3; if ( pwFile != NULL ) { while ( flock( fileno(pwFile), LOCK_EX | LOCK_NB ) == -1 && tries-- > 0 ) usleep( 25000 ); } } void CAuthFileBase::pwUnlock(void) { if ( pwFile != NULL ) flock( fileno(pwFile), LOCK_UN ); } void CAuthFileBase::pwWait(void) { // override in sub-class } void CAuthFileBase::pwSignal(void) { // override in sub-class } void CAuthFileBase::rsaWait(void) { // override in sub-class } void CAuthFileBase::rsaSignal(void) { // override in sub-class } int CAuthFileBase::getHeader( PWFileHeader *outHeader, bool inCanUseCachedCopy ) { int err = -1; ssize_t readCount; if ( outHeader == NULL ) return -1; if ( inCanUseCachedCopy && fGotHeader ) { memcpy( outHeader, &pwFileHeader, sizeof(PWFileHeader) ); return 0; } pwWait(); // not going to use mapping, too many crashes, not enough time to qualify //err = this->openPasswordFile( "r", true ); //err = this->openPasswordFile( "r", false ); err = this->openPasswordFile( "r+", false ); if ( err == 0 && pwFile ) { if ( pwFileBasePtr != NULL ) { // get from the map memcpy( outHeader, pwFileBasePtr, sizeof(PWFileHeader) ); } else { /* err = fseek( pwFile, 0, SEEK_SET ); if ( err == 0 ) readCount = fread( outHeader, sizeof(PWFileHeader), 1, pwFile ); */ // This one is faster (Panther7A122) readCount = pread( fileno(pwFile), outHeader, sizeof(PWFileHeader), 0 ); } // adopt the new header data memcpy( &pwFileHeader, outHeader, sizeof(PWFileHeader) ); fGotHeader = true; } pwSignal(); return err; } int CAuthFileBase::setHeader( const PWFileHeader *inHeader ) { int err = -1; long writeCount; if ( inHeader == NULL ) return -1; pwWait(); err = this->openPasswordFile( "r+", false ); if ( err == 0 && pwFile ) { err = fseek( pwFile, 0, SEEK_SET ); if ( err == 0 ) { // adopt the new header data if ( inHeader != &pwFileHeader ) memcpy( &pwFileHeader, inHeader, sizeof(PWFileHeader) ); // write to disk writeCount = fwrite( &pwFileHeader, sizeof(PWFileHeader), 1, pwFile ); if ( writeCount != 1 ) { err = -1; } fflush( pwFile ); } } pwSignal(); return err; } //---------------------------------------------------------------------------------------------------- // getRSAPublicKey // // Returns a base64 encoded rsa key //---------------------------------------------------------------------------------------------------- int CAuthFileBase::getRSAPublicKey( char *outRSAKeyStr ) { PWFileHeader dbHeader; int result = 0; long len; if ( outRSAKeyStr == NULL ) return -1; *outRSAKeyStr = '\0'; result = this->getHeader( &dbHeader, true ); if ( result == 0 ) { 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 CAuthFileBase::loadRSAKeys( void ) { int result = 0; #if COMPILE_WITH_RSA_LOAD PWFileHeader dbHeader; char passphrase[1] = ""; // check if we already loaded the key rsaWait(); if ( rsaKey != NULL ) { rsaSignal(); return 1; } result = this->getHeader( &dbHeader, true ); if ( result == 0 ) { int check1, check2, cipher_type; off_t len; Buffer buffer, decrypted; char *cp; CipherContext cipher; BN_CTX *ctx; BIGNUM *aux; time_t now; len = dbHeader.privateKeyLen; buffer_init(&buffer); buffer_append_space(&buffer, &cp, len); memcpy(cp, dbHeader.privateKey, len); /* Check that it is at least big enought to contain the ID string. */ if (len < strlen(AUTHFILE_ID_STRING) + 1) { syslog(LOG_INFO, "Bad key."); buffer_free(&buffer); rsaSignal(); return 0; } /* * Make sure it begins with the id string. Consume the id string * from the buffer. */ for (unsigned int i = 0; i < (unsigned int) strlen(AUTHFILE_ID_STRING) + 1; i++) if (buffer_get_char(&buffer) != (unsigned char) AUTHFILE_ID_STRING[i]) { syslog(LOG_INFO, "Bad key."); buffer_free(&buffer); 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(); 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: BN_clear_free(rsaKey->n); rsaKey->n = NULL; BN_clear_free(rsaKey->e); rsaKey->e = NULL; //if (comment_return) // xfree(*comment_return); rsaSignal(); return 0; } /* Read the rest of the private key. */ rsaKey->d = BN_new(); 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)); rsaSignal(); return 1; } bzero(&dbHeader, sizeof(dbHeader)); rsaSignal(); #else syslog(LOG_INFO, "RSA key loading not compiled\n"); result = -1; #endif return result; } //---------------------------------------------------------------------------------------------------- // decryptRSA // // Returns: -1=fail, 0=success //---------------------------------------------------------------------------------------------------- int CAuthFileBase::decryptRSA( unsigned char *inBlob, int inBlobLen, unsigned char *outBlob ) { int len; int result = 0; if ( this->loadRSAKeys() != 1 ) return -1; rsaWait(); len = RSA_private_decrypt( inBlobLen, inBlob, outBlob, rsaKey, RSA_PKCS1_PADDING ); 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_INFO, "rsa_private_decrypt() failed" ); result = -1; // let's try reloading the key rsaWait(); RSA_free( rsaKey ); rsaKey = NULL; rsaSignal(); if ( this->loadRSAKeys() == 1 ) { rsaWait(); len = RSA_private_decrypt( inBlobLen, inBlob, outBlob, rsaKey, RSA_PKCS1_PADDING ); rsaSignal(); if ( len > 0 ) result = 0; } } return result; } //---------------------------------------------------------------------------------------------------- // encryptRSA // // Returns: -1=fail, or length of encrypted part of //---------------------------------------------------------------------------------------------------- int CAuthFileBase::encryptRSA( unsigned char *inBlob, int inBlobLen, unsigned char *outBlob ) { int len; int maxRSASize; if ( this->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; rsaWait(); len = RSA_public_encrypt( inBlobLen, inBlob, outBlob, rsaKey, RSA_PKCS1_PADDING ); rsaSignal(); if ( len <= 0 ) { //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 CAuthFileBase::isWeakAuthMethod( const char *inMethod ) { int index; int result; PWFileHeader dbHeader; result = this->getHeader( &dbHeader, true ); if ( result != 0 ) return 1; for ( index = 0; index < kPWFileMaxWeakMethods; index++ ) if ( strcmp( inMethod, dbHeader.weakAuthMethods[index].method ) == 0 ) return 1; return 0; } int CAuthFileBase::addWeakAuthMethod( const char *inMethod ) { int index; PWFileHeader ourHeader; int result = 0; pwLock(); result = this->getHeader( &ourHeader ); if ( result == 0 ) { for ( index = 0; index < kPWFileMaxWeakMethods; index++ ) { if ( ourHeader.weakAuthMethods[index].method[0] == 0 ) { strcpy( ourHeader.weakAuthMethods[index].method, inMethod ); result = this->setHeader( &ourHeader ); break; } } } pwUnlock(); return result; } int CAuthFileBase::removeWeakAuthMethod( const char *inMethod ) { int index; PWFileHeader ourHeader; int result = 0; pwLock(); result = this->getHeader( &ourHeader ); if ( result == 0 ) { for ( index = 0; index < kPWFileMaxWeakMethods; index++ ) { if ( strcmp( inMethod, pwFileHeader.weakAuthMethods[index].method ) == 0 ) { bzero( pwFileHeader.weakAuthMethods[index].method, SASL_MECHNAMEMAX+1 ); result = this->setHeader( &ourHeader ); break; } } } 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 CAuthFileBase::expandDatabase( unsigned long inNumSlots, long *outSlot ) { int err; int writeCount; pwWait(); err = this->openPasswordFile( "r+", false ); if ( err == 0 && pwFile != NULL ) { // write blank space err = fseek( pwFile, 0, SEEK_END ); if ( err == 0 ) { PWFileEntry anEntry; int i; bzero( &anEntry, sizeof(PWFileEntry) ); for ( i = inNumSlots; i > 0; i-- ) { writeCount = fwrite( &anEntry, sizeof(PWFileEntry), 1, pwFile ); if ( writeCount != 1 ) { err = -1; break; } } } // update header if ( err == 0 ) err = fseek( pwFile, 0, SEEK_SET ); if ( err == 0 ) { pwFileHeader.numberOfSlotsCurrentlyInFile += inNumSlots; if ( outSlot != NULL ) { pwFileHeader.deepestSlotUsed++; pwFileHeader.deepestSlotUsedByThisServer = pwFileHeader.deepestSlotUsed; *outSlot = pwFileHeader.deepestSlotUsed; } writeCount = fwrite( &pwFileHeader, sizeof(PWFileHeader), 1, pwFile ); if ( writeCount != 1 ) { err = -1; } } } pwSignal(); return err; } //---------------------------------------------------------------------------------------------------- // nextSlot // // Returns: 0 for invalid/error, or the next slot number in the pw file for writing the next entry. //---------------------------------------------------------------------------------------------------- long CAuthFileBase::nextSlot(void) { long slot = 0; int err = -1; off_t curpos; long readCount; PWFileEntry dbEntry; if ( pwFileValidated ) { if ( pwFileHeader.deepestSlotUsedByThisServer < pwFileHeader.numberOfSlotsCurrentlyInFile - 1 ) { err = this->getPasswordRec( pwFileHeader.deepestSlotUsedByThisServer + 1, &dbEntry ); if ( err == 0 && dbEntry.time == 0 && dbEntry.rnd == 0 && dbEntry.sequenceNumber == 0 && dbEntry.slot == 0 ) { pwFileHeader.deepestSlotUsedByThisServer++; slot = pwFileHeader.deepestSlotUsedByThisServer; if ( pwFileHeader.deepestSlotUsedByThisServer > pwFileHeader.deepestSlotUsed ) pwFileHeader.deepestSlotUsed = pwFileHeader.deepestSlotUsedByThisServer; return slot; } } if ( pwFileHeader.deepestSlotUsed < pwFileHeader.numberOfSlotsCurrentlyInFile - 1 ) { pwFileHeader.deepestSlotUsed++; pwFileHeader.deepestSlotUsedByThisServer = pwFileHeader.deepestSlotUsed; slot = pwFileHeader.deepestSlotUsed; } else { // go look in the freelist freeListFile = fopen( kFreeListFilePath, "r+" ); if ( freeListFile ) { err = fseek( freeListFile, -sizeof(long), SEEK_END ); if ( err == 0 ) { curpos = ftell( freeListFile ); readCount = fread( &slot, sizeof(long), 1, freeListFile ); this->closeFreeListFile(); if ( readCount == 1 ) { // snip the one we used err = truncate( kFreeListFilePath, curpos ); } else { err = -1; } } } // if freelist is empty, expand the file if ( err != 0 ) { err = this->expandDatabase( kPWFileInitialSlots, &slot ); } } } return slot; } //---------------------------------------------------------------------------------------------------- // getGMTime // // Returns: a time struct based on GMT //---------------------------------------------------------------------------------------------------- void CAuthFileBase::getGMTime(struct tm *inOutGMT) { fUtils.getGMTime( inOutGMT ); } //---------------------------------------------------------------------------------------------------- // getTimeForRef // // Returns: a timestamp based on GMT //---------------------------------------------------------------------------------------------------- UInt32 CAuthFileBase::getTimeForRef(void) { time_t theTime; time(&theTime); return (UInt32)theTime; } //---------------------------------------------------------------------------------------------------- // getRandom // // Returns: a random number for user IDs //---------------------------------------------------------------------------------------------------- UInt32 CAuthFileBase::getRandom(void) { UInt32 result; UInt32 uiNow; time_t now; result = (UInt32) random(); time(&now); uiNow = (UInt32)now + result; srandom(uiNow); return result; } //---------------------------------------------------------------------------------------------------- // addRSAKeys // // Returns: 0 or -1 // Adds RSA version 2 keys to the password database header using the ssh-keygen tool //---------------------------------------------------------------------------------------------------- int CAuthFileBase::addRSAKeys(void) { char commandStr[256]; FILE *aFile; struct stat sb; int result; unsigned char *publicKey; unsigned long publicKeyLen; unsigned char *privateKey; unsigned long privateKeyLen; // make the keys sprintf(commandStr, "ssh-keygen -t rsa1 -b 1024 -f %s -P \"\"", kTempKeyFile); aFile = popen( commandStr, "r" ); if ( !aFile ) return -1; // read the data back (but there's nothing to read) pclose(aFile); // stat the key file to make sure we created it result = stat( kTempKeyFile, &sb ); if ( result != 0 ) return result; // add the private key aFile = fopen( kTempKeyFile, "r" ); if ( !aFile ) return -1; privateKeyLen = (unsigned long)sb.st_size; privateKey = (unsigned char *) malloc( privateKeyLen + 1 ); fread((char*)privateKey, (unsigned long)sb.st_size, 1, aFile); fclose(aFile); // stat the public key file to make sure we created it sprintf(commandStr, "%s.pub", kTempKeyFile); result = stat( commandStr, &sb ); if ( result != 0 ) return result; // add the public key aFile = fopen( commandStr, "r" ); if ( !aFile ) return -1; publicKeyLen = (unsigned long)sb.st_size; publicKey = (unsigned char *) malloc( publicKeyLen + 1 ); fread(publicKey, (unsigned long)sb.st_size, 1, aFile); fclose(aFile); result = this->addRSAKeys( publicKey, publicKeyLen, privateKey, privateKeyLen ); // we are done with these remove(commandStr); remove(kTempKeyFile); free(privateKey); 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 CAuthFileBase::addRSAKeys( unsigned char *publicKey, unsigned long publicKeyLen, unsigned char *privateKey, unsigned long privateKeyLen ) { PWFileHeader ourHeader; int result; if ( privateKeyLen > kPWFileMaxPrivateKeyBytes ) return -1; if ( publicKeyLen > kPWFileMaxPublicKeyBytes ) return -1; // retrieve the pw database header result = this->getHeader( &ourHeader ); if ( result != 0 ) 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 = this->setHeader( &ourHeader ); // do not leave the private key sitting around in the stack bzero(&ourHeader, sizeof(ourHeader)); return result; } //---------------------------------------------------------------------------------------------------- // addGenesisPassword // // 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 CAuthFileBase::addGenesisPassword(const char *username, const char *password, 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; // TEMP DISABLE 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" ); pwLock(); err = this->getHeader( &dbHeader ); if ( err == 0 ) { // 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 = this->setPasswordAtSlot( &passwordRec, passwordRec.slot ); if ( err == 0 && outPWRec != NULL ) { memcpy( outPWRec, &passwordRec, sizeof(PWFileEntry) ); } err2 = this->setHeader( &dbHeader ); } pwUnlock(); if ( err == 0 && err2 != 0 ) err = err2; return err; } //---------------------------------------------------------------------------------------------------- // addPassword // // Returns: errno // Used to add new passwords //---------------------------------------------------------------------------------------------------- int CAuthFileBase::addPassword(PWFileEntry *passwordRec, bool obfuscate) { PWFileHeader ignoreHeader; int err, err2; pwLock(); // refresh the header // the retrieved header is ignored because the nextSlot() method uses // the object's copy of the header in pwFileHeader. err = this->getHeader( &ignoreHeader ); if ( err != 0 ) return err; passwordRec->time = this->getTimeForRef(); passwordRec->rnd = this->getRandom(); passwordRec->sequenceNumber = ++pwFileHeader.sequenceNumber; passwordRec->slot = this->nextSlot(); fUtils.getGMTime( (struct tm *)&passwordRec->creationDate ); memcpy( &passwordRec->lastLogin, &passwordRec->creationDate, sizeof(struct tm) ); memcpy( &passwordRec->modDateOfPassword, &passwordRec->creationDate, sizeof(struct tm) ); err = this->setPasswordAtSlot( passwordRec, passwordRec->slot, obfuscate ); // re-write the header to mark the slot used. err2 = this->setHeader( &pwFileHeader ); pwUnlock(); if ( err == 0 && err2 != 0 ) err = err2; return err; } //---------------------------------------------------------------------------------------------------- // addPasswordAtSlot // // Returns: errno // Used to add password records from replicas. Fills in the slot if free; otherwise redirects the // record to the spill-bucket. //---------------------------------------------------------------------------------------------------- int CAuthFileBase::addPasswordAtSlot(PWFileEntry *passwordRec, long slot, bool obfuscate, bool setModDate) { PWFileEntry dbEntry; int err; bool bGoesInMainDB = false; // verifying the slot id, do not need to un-obfuscate err = this->getPasswordRec( passwordRec->slot, &dbEntry, false ); 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 = true; } else if ( dbEntry.time == 0 && dbEntry.rnd == 0 && dbEntry.sequenceNumber == 0 && dbEntry.slot == 0 ) { // slot free bGoesInMainDB = true; } if ( bGoesInMainDB ) { err = this->setPasswordAtSlot( passwordRec, passwordRec->slot, obfuscate, setModDate ); } else { err = this->SaveOverflowRecord( passwordRec, obfuscate, setModDate ); } return err; } //---------------------------------------------------------------------------------------------------- // setPasswordAtSlot // // Returns: errno // Used to write to a specific slot. //---------------------------------------------------------------------------------------------------- int CAuthFileBase::setPasswordAtSlot(PWFileEntry *passwordRec, long slot, bool obfuscate, bool setModDate) { long offset; int err = -1; int writeCount; unsigned int encodeLen; if ( slot > 0 ) { if ( setModDate ) fUtils.getGMTime( (struct tm *)&passwordRec->modificationDate ); pwWait(); err = this->openPasswordFile( "r+", false ); if ( err == 0 && pwFile ) { offset = fUtils.slotToOffset( slot ); err = fseek( pwFile, offset, SEEK_SET ); if ( err == 0 ) { //passwordRec->slot = slot; encodeLen = strlen(passwordRec->passwordStr); encodeLen += (kFixedDESChunk - (encodeLen % kFixedDESChunk)); if ( encodeLen > sizeof(passwordRec->passwordStr) ) encodeLen = sizeof(passwordRec->passwordStr); if ( obfuscate ) fUtils.DESEncode(kFixedDESKey, passwordRec->passwordStr, encodeLen); writeCount = fwrite( passwordRec, sizeof(PWFileEntry), 1, pwFile ); if ( obfuscate ) fUtils.DESDecode(kFixedDESKey, passwordRec->passwordStr, encodeLen); if ( writeCount != 1 ) err = -1; } /* memcpy( pwFileBasePtr + offset, passwordRec, sizeof(PWFileEntry) ); memcpy( pwFileBasePtr, &pwFileHeader, sizeof(PWFileHeader) ); */ } pwSignal(); } return err; } #if 0 // -------------------------------------------------------------------------------- // 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 CAuthFileBase::addHashes( const char *inRealm, PWFileEntry *inOutPasswordRec ) { unsigned char smbntHash[32]; unsigned char smblmHash[16]; long pwLen; // SMB-NT [ 0 ] CalculateSMBNTHash(inOutPasswordRec->passwordStr, smbntHash); strcpy( inOutPasswordRec->digest[0].method, kSMBNTStorageTag ); inOutPasswordRec->digest[0].digest[0] = 64; ConvertBinaryToHex( smbntHash, 32, &inOutPasswordRec->digest[0].digest[1] ); // SMB-LAN-MANAGER [ 1 ] CalculateSMBLANManagerHash(inOutPasswordRec->passwordStr, smblmHash); strcpy( inOutPasswordRec->digest[1].method, "*cmusaslsecretSMBLM" ); inOutPasswordRec->digest[1].digest[0] = 32; ConvertBinaryToHex( smblmHash, 16, &inOutPasswordRec->digest[1].digest[1] ); // DIGEST-MD5 [ 2 ] pwLen = strlen(inOutPasswordRec->passwordStr); { HASH HA1; char userID[35]; this->passwordRecRefToString( inOutPasswordRec, userID ); DigestCalcSecret( (unsigned char *)userID, (unsigned char *)inRealm, (unsigned char *)inOutPasswordRec->passwordStr, pwLen, HA1 ); /* * A1 = { H( { username-value, ":", realm-value, ":", passwd } ), * ":", nonce-value, ":", cnonce-value } */ // not enough room to store "*cmusaslsecretDIGEST-MD5" so truncate to 20 chars strncpy( inOutPasswordRec->digest[2].method, "*cmusaslsecretDIGEST-MD5", SASL_MECHNAMEMAX ); inOutPasswordRec->digest[2].method[SASL_MECHNAMEMAX] = '\0'; inOutPasswordRec->digest[2].digest[0] = HASHLEN; memcpy( &inOutPasswordRec->digest[2].digest[1], HA1, HASHLEN ); } // CRAM-MD5 [ 3 ] { HMAC_MD5_STATE state; hmac_md5_precalc( &state, (unsigned char *)inOutPasswordRec->passwordStr, pwLen ); strncpy( inOutPasswordRec->digest[3].method, "*cmusaslsecretCRAM-MD5", SASL_MECHNAMEMAX ); inOutPasswordRec->digest[3].method[SASL_MECHNAMEMAX] = '\0'; inOutPasswordRec->digest[3].digest[0] = sizeof(HMAC_MD5_STATE); memcpy( &inOutPasswordRec->digest[3].digest[1], &state, sizeof(HMAC_MD5_STATE) ); } // KERBEROS [ 4 ] // Kerberos doesn't currently store a hash here, we just store the realm name. // combined with the user name, we can call the KDC to get the kerberos hashes } #endif //----------------------------------------------------------------------------- // ConvertBinaryToHex //----------------------------------------------------------------------------- bool CAuthFileBase::ConvertBinaryToHex( const unsigned char *inData, long len, char *outHexStr ) { bool result = true; char *tptr = outHexStr; char base16table[16] = { '0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F' }; if ( inData == nil || outHexStr == nil ) return false; for ( int idx = 0; idx < len; idx++ ) { *tptr++ = base16table[(inData[idx] >> 4) & 0x0F]; *tptr++ = base16table[(inData[idx] & 0x0F)]; } *tptr = '\0'; return result; } int CAuthFileBase::getPasswordRec(long slot, PWFileEntry *passRec, bool unObfuscate) { long offset; int err = -1; ssize_t readCount; if ( slot > 0 ) { pwWait(); //err = this->openPasswordFile( "r", false ); err = this->openPasswordFile( "r+", false ); if ( err == 0 && pwFile ) { offset = fUtils.slotToOffset( slot ); if ( pwFileBasePtr ) { // file is memory-mapped memcpy( passRec, pwFileBasePtr + offset, sizeof(PWFileEntry) ); } else { readCount = pread( fileno(pwFile), passRec, sizeof(PWFileEntry), offset ); if ( readCount != sizeof(PWFileEntry) ) { // failure could indicate a problem with the file descriptor // get a new one next time this->closePasswordFile(); err = -2; } } // recover the password if ( unObfuscate ) fUtils.DESAutoDecode( kFixedDESKey, passRec->passwordStr ); } pwSignal(); } return err; } //---------------------------------------------------------------------------------------------------- // getValidPasswordRec // // Returns: errno // same as getPasswordRec but validates the record's ref numbers. //---------------------------------------------------------------------------------------------------- int CAuthFileBase::getValidPasswordRec(PWFileEntry *passwordRec, bool *outFromSpillBucket, bool unObfuscate) { int err; PWFileEntry dbEntry; if ( outFromSpillBucket != NULL ) *outFromSpillBucket = false; err = this->getPasswordRec( passwordRec->slot, &dbEntry, 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 = this->getPasswordRecFromSpillBucket( passwordRec, &dbEntry ); if ( err == 0 ) { if ( passwordRec->time == dbEntry.time && passwordRec->rnd == dbEntry.rnd && passwordRec->sequenceNumber == dbEntry.sequenceNumber && passwordRec->slot == dbEntry.slot ) { memcpy( passwordRec, &dbEntry, sizeof(PWFileEntry) ); if ( outFromSpillBucket != NULL ) *outFromSpillBucket = true; } else { err = -3; } } else { err = -3; } } // invalidate the data bzero( &dbEntry, sizeof(PWFileEntry) ); return err; } int CAuthFileBase::freeSlot(PWFileEntry *passwordRec) { int err; long slot = passwordRec->slot; long writeCount; PWFileEntry deleteRec; bool fromSpillBucket; // original rec must be valid to have permission to clear a slot err = this->getValidPasswordRec( passwordRec, &fromSpillBucket ); if ( err == 0 ) { // start with a zero record bzero( &deleteRec, sizeof(PWFileEntry) ); // keep the ID, mark the time of deletion in modDateOfPassword, // and mark dead. deleteRec.time = passwordRec->time; deleteRec.rnd = passwordRec->rnd; deleteRec.sequenceNumber = passwordRec->sequenceNumber; deleteRec.slot = passwordRec->slot; fUtils.getGMTime( (struct tm *)&deleteRec.modDateOfPassword ); deleteRec.recordIsDead = true; if ( fromSpillBucket ) err = this->SaveOverflowRecord( &deleteRec ); else err = this->setPasswordAtSlot( &deleteRec, slot ); // add the slot number to free list freeListFile = fopen( kFreeListFilePath, "a+" ); if ( freeListFile ) { writeCount = fwrite( &slot, sizeof(long), 1, freeListFile ); if ( writeCount != 1 ) { // may have a forgotten slot err = -1; } this->closeFreeListFile(); } } return err; } //---------------------------------------------------------------------------------------------------- // passwordRecRefToString //---------------------------------------------------------------------------------------------------- void CAuthFileBase::passwordRecRefToString(PWFileEntry *inPasswordRec, char *outRefStr) { fUtils.passwordRecRefToString( inPasswordRec, outRefStr ); } //---------------------------------------------------------------------------------------------------- // stringToPasswordRecRef // // Returns: Boolean (1==valid ref, 0==fail) //---------------------------------------------------------------------------------------------------- int CAuthFileBase::stringToPasswordRecRef(const char *inRefStr, PWFileEntry *outPasswordRec) { return fUtils.stringToPasswordRecRef( inRefStr, outPasswordRec ); } //------------------------------------------------------------------------------------------------ // getUserIDFromName // // Returns: Boolean (1==found, 0=not found) //------------------------------------------------------------------------------------------------ int CAuthFileBase::getUserIDFromName(const char *inName, bool inAllUsers, long inMaxBuffSize, char *outID) { PWFileHeader dbHeader; int result = 0; int err = 0; UInt32 index; PWFileEntry passRec; char theAdminID[256]; long buffRemaining = inMaxBuffSize; long len; if ( outID == NULL || buffRemaining < 1 ) return 0; *outID = '\0'; buffRemaining--; err = this->getHeader( &dbHeader, true ); if ( err != 0 ) return result; for ( index = dbHeader.deepestSlotUsed; index > 0; index-- ) { // Not checking passwords, so leave them obfuscated for performance err = this->getPasswordRec( index, &passRec, false ); if ( err != 0 ) break; if ( (inAllUsers || passRec.access.isAdminUser) && !passRec.access.isDisabled && strcmp( inName, passRec.usernameStr ) == 0 ) { if ( result == 1 ) { if ( buffRemaining < 1 ) break; strcat( outID, ";" ); buffRemaining--; } fUtils.passwordRecRefToString( &passRec, theAdminID ); len = strlen( theAdminID ); if ( buffRemaining <= len ) break; strcat( outID, theAdminID ); buffRemaining -= len; result = 1; } } // if no user was found, go troll through the spill-bucket if ( result == 0 ) { // not implemented } return result; } #pragma mark - //------------------------------------------------------------------------------------------------ // AddPassword //------------------------------------------------------------------------------------------------ int CAuthFileBase::AddPassword( const char *inUser, const char *inPassword, 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 = false; anEntry.access.isAdminUser = false; anEntry.access.newPasswordRequired = false; anEntry.access.usingHistory = false; anEntry.access.canModifyPasswordforSelf = true; anEntry.access.usingExpirationDate = false; anEntry.access.usingHardExpirationDate = false; anEntry.access.requiresAlpha = false; anEntry.access.requiresNumeric = false; anEntry.access.passwordIsHash = false; anEntry.access.maxMinutesOfNonUse = 0; anEntry.access.maxFailedLoginAttempts = 0; anEntry.access.minChars = 0; anEntry.access.maxChars = 0; strcpy( anEntry.usernameStr, inUser ); strncpy( anEntry.passwordStr, inPassword, sizeof(anEntry.passwordStr) ); result = this->addPassword( &anEntry ); fUtils.passwordRecRefToString( &anEntry, refStr ); strcpy( outPasswordRef, refStr ); return result; } #if 0 //------------------------------------------------------------------------------------------------ // MakeSyncFile //------------------------------------------------------------------------------------------------ int CAuthFileBase::MakeSyncFile( const char *inFileName, time_t inAfterDate, long inTimeSkew, long *outNumRecordsUpdated ) { PWFileHeader dbHeader; int result = 0; int err; UInt32 index; PWFileEntry passRec; CKerberosPrincipal* kerberosRec; time_t theTime; FILE *syncFile; int writeCount; int zeroLen = 0; CKerberosPrincipalList kerbList; // sanity if ( inFileName == NULL ) return -1; if ( outNumRecordsUpdated != NULL ) *outNumRecordsUpdated = 0; // create sync file syncFile = fopen( inFileName, "w+" ); if ( syncFile == NULL ) return -1; err = chmod( inFileName, S_IRUSR | S_IWUSR ); // load/copy header try { kerbList.ReadAllPrincipalsFromDB(); writeCount = fwrite( &inAfterDate, sizeof(inAfterDate), 1, syncFile ); if ( writeCount != 1 ) throw( -1 ); err = this->getHeader(&dbHeader); if ( err != 0 ) throw( err ); writeCount = fwrite( &dbHeader, sizeof(dbHeader), 1, syncFile ); if ( writeCount != 1 ) throw( -1 ); // copy records after the sync date for ( index = dbHeader.deepestSlotUsed; index > 0; index-- ) { err = this->getPasswordRec( index, &passRec, false ); if ( err != 0 ) throw( err ); // adjust time skew for comparison purposes. The record itself is // adjusted on the processing side. theTime = ::timegm( (struct tm *)&passRec.modificationDate ) + inTimeSkew; if ( theTime >= inAfterDate ) { writeCount = fwrite( &passRec, sizeof(passRec), 1, syncFile ); if ( writeCount != 1 ) throw( -1 ); if (strlen(passRec.digest[4].digest) > 0) { char principalName[600]; strcpy(principalName, passRec.usernameStr); strcat(principalName, "@"); strcat(principalName, passRec.digest[4].digest); kerberosRec = kerbList.GetPrincipalByName(principalName); } else kerberosRec = NULL; if (kerberosRec != NULL) { writeCount = kerberosRec->WritePrincipalToFile(syncFile); delete kerberosRec; } else writeCount = fwrite( &zeroLen, sizeof(zeroLen), 1, syncFile ); if ( writeCount != 1 ) throw( -1 ); if ( outNumRecordsUpdated != NULL ) (*outNumRecordsUpdated)++; } } // look in the spill-bucket this->AddOverflowToSyncFile( syncFile, inAfterDate, inTimeSkew, outNumRecordsUpdated ); // 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 memset(&passRec, 0, sizeof(passRec)); passRec.slot = (UInt32)-1; kerbList.DeleteOldPrincipals(inAfterDate); index = 0; kerberosRec = kerbList.GetPrincipalByIndex(index++); while (kerberosRec != NULL) { writeCount = fwrite( &passRec, sizeof(passRec), 1, syncFile ); if ( writeCount != 1 ) throw( -1 ); writeCount = kerberosRec->WritePrincipalToFile(syncFile); if ( writeCount != 1 ) throw( -1 ); kerberosRec = kerbList.GetPrincipalByIndex(index++); } } catch( int error ) { result = error; } fclose( syncFile ); bzero( &dbHeader, sizeof(dbHeader) ); bzero( &passRec, sizeof(passRec) ); return result; } #endif //------------------------------------------------------------------------------------------------ // GetSyncTimeFromSyncFile // // Returns: 0 == success, -1 == fail // // This method gets the official sync time (watermark) for a data file //------------------------------------------------------------------------------------------------ int CAuthFileBase::GetSyncTimeFromSyncFile( const char *inSyncFile, time_t *outSyncTime ) { int err; FILE *syncFile; int readCount; unsigned long remoteFileLen; struct stat sb; time_t syncTime = 0; // sanity if ( inSyncFile == NULL || outSyncTime == NULL ) return -1; *outSyncTime = 0; // get file len remoteFileLen = 0; err = lstat( inSyncFile, &sb ); if ( err == 0 ) remoteFileLen = sb.st_size; if ( remoteFileLen < sizeof(PWFileHeader) ) return -1; // open the sync file syncFile = fopen( inSyncFile, "r" ); if ( syncFile == NULL ) return -1; // read opposing sync time readCount = fread( &syncTime, sizeof(syncTime), 1, syncFile ); if ( readCount == 1 ) *outSyncTime = syncTime; fclose( syncFile ); return 0; } #if 0 //------------------------------------------------------------------------------------------------ // ProcessSyncFile // // Returns: 0 == success, -1 == fail, -2 == incompatible databases // // 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 CAuthFileBase::ProcessSyncFile( const char *inSyncFile, long inTimeSkew, long *outNumAccepted, long *outNumTrumped ) { int result = 0; int err; UInt32 index; FILE *syncFile; time_t localTime, remoteTime; PWFileHeader localHeader, remoteHeader; PWFileEntry localRec, remoteRec; int readCount; unsigned long remoteFileLen; struct stat sb; time_t syncTime = 0; bool bFromSpillBucket; bool bNeedsUpdate; int kerbRecordLen; CKerberosPrincipal* remoteKerbRec; CKerberosPrincipal* localKerbRec; CKerberosPrincipalList kerbList; // sanity if ( inSyncFile == NULL ) return -1; if ( outNumAccepted != NULL ) *outNumAccepted = 0; if ( outNumTrumped != NULL ) *outNumTrumped = 0; // get file len remoteFileLen = 0; err = lstat( inSyncFile, &sb ); if ( err == 0 ) remoteFileLen = sb.st_size; if ( remoteFileLen < sizeof(PWFileHeader) ) return -1; // open the sync file syncFile = fopen( inSyncFile, "r" ); if ( syncFile == NULL ) return -1; pwLock(); try { // copy our header err = this->getHeader( &localHeader ); if ( err != 0 ) throw( err ); // read opposing sync time readCount = fread( &syncTime, sizeof(syncTime), 1, syncFile ); if ( readCount != 1 ) throw( -1 ); // read opposing header readCount = fread( &remoteHeader, sizeof(remoteHeader), 1, syncFile ); if ( readCount != 1 ) throw( -1 ); // check compatibility if ( localHeader.signature != remoteHeader.signature || localHeader.version != remoteHeader.version || localHeader.entrySize != remoteHeader.entrySize ) { throw( -2 ); } // sync the header localHeader.sequenceNumber = Max(localHeader.sequenceNumber, remoteHeader.sequenceNumber); localHeader.deepestSlotUsed = Max(localHeader.deepestSlotUsed, remoteHeader.deepestSlotUsed); if ( remoteHeader.accessModDate > localHeader.accessModDate ) localHeader.access = remoteHeader.access; err = this->setHeader( &localHeader ); if ( remoteHeader.numberOfSlotsCurrentlyInFile > localHeader.numberOfSlotsCurrentlyInFile ) this->expandDatabase( remoteHeader.numberOfSlotsCurrentlyInFile - localHeader.numberOfSlotsCurrentlyInFile, NULL ); // either update the record or trump it for ( index = (remoteFileLen - sizeof(PWFileHeader) - sizeof(time_t))/sizeof(PWFileEntry); index > 0; index-- ) { bNeedsUpdate = false; readCount = fread( &remoteRec, sizeof(remoteRec), 1, syncFile ); if ( readCount != 1 ) throw( -1 ); readCount = fread( &kerbRecordLen, sizeof(kerbRecordLen), 1, syncFile ); if ( readCount != 1 ) throw( -1 ); localKerbRec = NULL; if (kerbRecordLen > 0) remoteKerbRec = kerbList.ReadPrincipalFromFile(syncFile, kerbRecordLen); else remoteKerbRec = NULL; if (remoteKerbRec != NULL) localKerbRec = CKerberosPrincipal::ReadPrincipalFromDB(remoteKerbRec->GetName()); localRec = remoteRec; if (remoteRec.slot != (UInt32)-1) { // this is a normal user record err = this->getValidPasswordRec( &localRec, &bFromSpillBucket, false ); if ( err != 0 ) { // record not in the database yet localRec = remoteRec; bNeedsUpdate = true; } // password fields localTime = ::timegm( (struct tm *)&localRec.modDateOfPassword ); remoteTime = ::timegm( (struct tm *)&remoteRec.modDateOfPassword ) - inTimeSkew; ::gmtime_r( &remoteTime, (struct tm *)&remoteRec.modDateOfPassword ); if ( remoteTime > localTime ) { memcpy( &localRec.modDateOfPassword, &remoteRec.modDateOfPassword, sizeof(BSDTimeStructCopy) ); memcpy( localRec.passwordStr, remoteRec.passwordStr, sizeof(localRec.passwordStr) ); for ( int idx = 0; idx < kPWFileMaxDigests; idx++ ) localRec.digest[idx] = remoteRec.digest[idx]; bNeedsUpdate = true; } else if (localKerbRec != NULL) remoteKerbRec->CopyPassword(localKerbRec); // last login time localTime = ::timegm( (struct tm *)&localRec.lastLogin ); remoteTime = ::timegm( (struct tm *)&remoteRec.lastLogin ) - inTimeSkew; ::gmtime_r( &remoteTime, (struct tm *)&remoteRec.lastLogin ); if ( remoteTime > localTime ) { memcpy( &localRec.lastLogin, &remoteRec.lastLogin, sizeof(BSDTimeStructCopy) ); bNeedsUpdate = true; } else if (localKerbRec != NULL) remoteKerbRec->CopyLastLogin(localKerbRec); // all non-special fields localTime = ::timegm( (struct tm *)&localRec.modificationDate ); remoteTime = ::timegm( (struct tm *)&remoteRec.modificationDate ) - inTimeSkew; ::gmtime_r( &remoteTime, (struct tm *)&remoteRec.modificationDate ); if ( remoteTime > localTime ) { 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.recordIsDead = remoteRec.recordIsDead; localRec.doNotReplicate = remoteRec.doNotReplicate; localRec.unused511 = remoteRec.unused511; bNeedsUpdate = true; } if ( bNeedsUpdate ) { if ( outNumAccepted != NULL ) (*outNumAccepted)++; err = this->addPasswordAtSlot( &localRec, localRec.slot, false, false ); } 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 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; } // remaining remote kerberos records will be delete along with the kerbList if (localKerbRec != NULL) delete localKerbRec; } // now sync kerberos information kerbList.WriteAllPrincipalsToDB(); } catch( int error ) { result = error; } pwUnlock(); fclose( syncFile ); // clear sensitive info bzero( &localHeader, sizeof(localHeader) ); bzero( &remoteHeader, sizeof(remoteHeader) ); bzero( &localRec, sizeof(localRec) ); bzero( &remoteRec, sizeof(remoteRec) ); return result; } #endif #pragma mark - #pragma mark ¥POLICY TESTING¥ #pragma mark - //------------------------------------------------------------------------------------------------ // DisableStatus // // Returns: kAuthOK, kAuthUserDisabled, kAuthPasswordExpired //------------------------------------------------------------------------------------------------ int CAuthFileBase::DisableStatus(PWFileEntry *inOutPasswordRec, Boolean *outChanged) { PWAccessFeatures *access; if ( inOutPasswordRec == NULL || outChanged == NULL ) return kAuthFail; *outChanged = false; access = &inOutPasswordRec->access; // do not disable administrators if ( access->isAdminUser ) return kAuthOK; if ( inOutPasswordRec->access.isDisabled ) return kAuthUserDisabled; // test policies in the user record if ( (access->maxFailedLoginAttempts > 0 && inOutPasswordRec->failedLoginAttempts >= access->maxFailedLoginAttempts) ) { // for failed login attempts, if the maximum is exceeded, set the isDisabled flag on the record // and reset so that the account can be re-enabled later. inOutPasswordRec->access.isDisabled = true; inOutPasswordRec->failedLoginAttempts = 0; *outChanged = true; return kAuthUserDisabled; } if ( (access->usingHardExpirationDate && TimeIsStale(&(access->hardExpireDateGMT))) || (access->maxMinutesUntilDisabled > 0 && LoginTimeIsStale( &inOutPasswordRec->creationDate, access->maxMinutesUntilDisabled )) ) { inOutPasswordRec->access.isDisabled = true; *outChanged = true; return kAuthUserDisabled; } // 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 && LoginTimeIsStale(&inOutPasswordRec->lastLogin, access->maxMinutesOfNonUse) ) return kAuthUserDisabled; // test policies in the global record if ( pwFileHeader.access.maxFailedLoginAttempts > 0 && inOutPasswordRec->failedLoginAttempts >= pwFileHeader.access.maxFailedLoginAttempts ) { // for failed login attempts, if the maximum is exceeded, set the isDisabled flag on the record // and reset so that the account can be re-enabled later. inOutPasswordRec->access.isDisabled = true; inOutPasswordRec->failedLoginAttempts = 0; *outChanged = true; return kAuthUserDisabled; } if ( (pwFileHeader.access.usingHardExpirationDate && TimeIsStale(&pwFileHeader.access.hardExpireDateGMT)) || (pwFileHeader.access.maxMinutesUntilDisabled > 0 && LoginTimeIsStale( &inOutPasswordRec->creationDate, pwFileHeader.access.maxMinutesUntilDisabled )) ) { inOutPasswordRec->access.isDisabled = true; *outChanged = true; return kAuthUserDisabled; } // 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 ( pwFileHeader.access.maxMinutesOfNonUse > 0 && LoginTimeIsStale(&inOutPasswordRec->lastLogin, pwFileHeader.access.maxMinutesOfNonUse) ) return kAuthUserDisabled; return kAuthOK; } //------------------------------------------------------------------------------------------------ // ChangePasswordStatus // // Returns: kAuthOK, kAuthPasswordNeedsChange, kAuthPasswordExpired //------------------------------------------------------------------------------------------------ int CAuthFileBase::ChangePasswordStatus(PWFileEntry *inPasswordRec) { if ( inPasswordRec->access.isAdminUser ) return kAuthOK; if ( (inPasswordRec->access.newPasswordRequired) || (inPasswordRec->access.usingExpirationDate && TimeIsStale( &inPasswordRec->access.expirationDateGMT )) || (inPasswordRec->access.maxMinutesUntilChangePassword > 0 && LoginTimeIsStale( &inPasswordRec->modDateOfPassword, inPasswordRec->access.maxMinutesUntilChangePassword )) || (pwFileHeader.access.usingExpirationDate && TimeIsStale( &pwFileHeader.access.expirationDateGMT )) || (pwFileHeader.access.maxMinutesUntilChangePassword > 0 && LoginTimeIsStale( &inPasswordRec->modDateOfPassword, pwFileHeader.access.maxMinutesUntilChangePassword )) ) { if ( inPasswordRec->access.canModifyPasswordforSelf || inPasswordRec->access.isAdminUser ) return kAuthPasswordNeedsChange; else return kAuthPasswordExpired; } // not implemented return kAuthOK; } //------------------------------------------------------------------------------------------------ // RequiredCharacterStatus // // Returns: enum of Reposonse Codes (CAuthFileCPP.h) //------------------------------------------------------------------------------------------------ int CAuthFileBase::RequiredCharacterStatus(PWFileEntry *inPasswordRec, const char *inPassword) { Boolean requiresAlpha = (inPasswordRec->access.requiresAlpha || pwFileHeader.access.requiresAlpha ); Boolean requiresNumeric = (inPasswordRec->access.requiresNumeric || pwFileHeader.access.requiresNumeric ); UInt16 minChars = (inPasswordRec->access.minChars > 0) ? inPasswordRec->access.minChars : pwFileHeader.access.minChars; UInt16 maxChars = (inPasswordRec->access.maxChars > 0) ? inPasswordRec->access.maxChars : pwFileHeader.access.maxChars; Boolean passwordCannotBeName = (inPasswordRec->access.passwordCannotBeName || pwFileHeader.access.passwordCannotBeName ); UInt16 len = strlen(inPassword); // The password server is not accepting blank passwords because some auth // methods, such as DIGEST-MD5, will not authenticate them. if ( len == 0 ) return kAuthPasswordTooShort; if ( len < minChars ) return kAuthPasswordTooShort; if ( maxChars > 0 && len > maxChars ) return kAuthPasswordTooLong; if ( requiresAlpha ) { Boolean hasAlpha = false; for ( int index = 0; index < len; index++ ) { if ( isalpha(inPassword[index]) ) { hasAlpha = true; break; } } if ( !hasAlpha ) return kAuthPasswordNeedsAlpha; } if ( requiresNumeric ) { Boolean hasDecimal = false; for ( int index = 0; index < len; index++ ) { if ( isdigit(inPassword[index]) ) { hasDecimal = true; break; } } if ( !hasDecimal ) return kAuthPasswordNeedsDecimal; } if ( passwordCannotBeName ) { UInt16 unameLen = strlen( inPasswordRec->usernameStr ); UInt16 smallerLen = ((len < unameLen) ? len : unameLen); // disallow the smaller substring, case-insensitive if ( strncasecmp( inPassword, inPasswordRec->usernameStr, smallerLen ) == 0 ) return kAuthPasswordNeedsChange; } return kAuthOK; } /*----------------------------------------------------------------------------------*/ #pragma mark - #pragma mark ¥SPILL-BUCKET ACCESSORS¥ #pragma mark - int CAuthFileBase::getPasswordRecFromSpillBucket(PWFileEntry *inRec, PWFileEntry *passRec) { PWFileEntry recBuff; off_t offset = 0; off_t byteCount; FILE *fp; int err = -1; char uidStr[35]; char buff[35]; unsigned int encodeLen; if ( inRec == NULL || passRec == NULL ) return -1; err = this->OpenOverflowFile( inRec, false, &fp ); if ( err != 0 ) return err; // use text-based matching to avoid endian problems fUtils.passwordRecRefToString( inRec, uidStr ); syslog(LOG_INFO, "looking in spillbucket for: %s", uidStr); do { byteCount = pread( fileno(fp), buff, sizeof(buff), offset ); syslog(LOG_INFO, "found %s", buff); if ( strncmp( uidStr, buff, 34 ) == 0 ) { syslog(LOG_INFO, "found it"); // found it byteCount = pread( fileno(fp), (char *)&recBuff, sizeof(recBuff), offset+34 ); // for any endian //fUtils.stringToPasswordRecRef( uidStr, recBuff ); // recover the password encodeLen = strlen(recBuff.passwordStr); encodeLen += (kFixedDESChunk - (encodeLen % kFixedDESChunk)); if ( encodeLen > sizeof(recBuff.passwordStr) ) encodeLen = sizeof(recBuff.passwordStr); fUtils.DESDecode(kFixedDESKey, recBuff.passwordStr, encodeLen); // copy the record memcpy( passRec, &recBuff, sizeof(PWFileEntry) ); // zero our copy memset( &recBuff, 0, sizeof(recBuff) ); err = 0; break; } offset += sizeof(PWFileEntry) + 34; } while ( byteCount == sizeof(uidStr) ); fclose( fp ); return err; } //------------------------------------------------------------------------------------------------ // SaveOverflowRecord // // Returns: -1, errno, or 0 for success // // Updates and existing record in the overflow bucket //------------------------------------------------------------------------------------------------ int CAuthFileBase::SaveOverflowRecord( PWFileEntry *inPasswordRec, bool obfuscate, bool setModDate ) { off_t offset = 0; off_t byteCount; FILE *fp; int err = -1; char uidStr[35]; char buff[35]; unsigned int encodeLen; int writeCount; if ( inPasswordRec == NULL ) return -1; if ( inPasswordRec->slot <= 0 ) return -1; err = this->OpenOverflowFile( inPasswordRec, true, &fp ); if ( err != 0 ) return err; // use text-based matching to avoid endian problems fUtils.passwordRecRefToString( inPasswordRec, uidStr ); if ( setModDate ) fUtils.getGMTime( (struct tm *)&inPasswordRec->modificationDate ); encodeLen = strlen(inPasswordRec->passwordStr); encodeLen += (kFixedDESChunk - (encodeLen % kFixedDESChunk)); if ( encodeLen > sizeof(inPasswordRec->passwordStr) ) encodeLen = sizeof(inPasswordRec->passwordStr); if ( obfuscate ) fUtils.DESEncode(kFixedDESKey, inPasswordRec->passwordStr, encodeLen); err = -1; do { byteCount = pread( fileno(fp), buff, sizeof(buff), offset ); if ( strncmp( uidStr, buff, 34 ) == 0 ) { // found it byteCount = pwrite( fileno(fp), inPasswordRec, sizeof(PWFileEntry), offset+34 ); err = 0; break; } offset += 34+sizeof(PWFileEntry); } while ( byteCount == sizeof(uidStr) ); // if not found, append the new record if ( err == -1 ) { err = fseek( fp, 0, SEEK_END ); if ( err == 0 ) { writeCount = fwrite( uidStr, 34, 1, fp ); if ( writeCount != 1 ) err = -1; } if ( err == 0 ) { writeCount = fwrite( inPasswordRec, sizeof(PWFileEntry), 1, fp ); if ( writeCount != 1 ) err = -1; } } if ( obfuscate ) fUtils.DESDecode(kFixedDESKey, inPasswordRec->passwordStr, encodeLen); fclose( fp ); return err; } //------------------------------------------------------------------------------------------------ // OpenOverflowFile //------------------------------------------------------------------------------------------------ int CAuthFileBase::OpenOverflowFile( PWFileEntry *inPasswordRec, bool create, FILE **outFP ) { char overflowFileName[50]; char overflowPath[1024]; FILE *fp; int err = -1; if ( inPasswordRec == NULL || outFP == NULL ) return -1; *outFP = NULL; this->PWRecToOverflowFileName( inPasswordRec, overflowFileName ); sprintf( overflowPath, "%s/%s", kPWDirPath, overflowFileName ); fp = fopen( overflowPath, create ? "a+" : "r+" ); if ( fp == NULL ) { err = errno; if ( err == 0 ) err = -1; return err; } *outFP = fp; return 0; } //------------------------------------------------------------------------------------------------ // PWRecToOverflowFileName // // Returns: void // outFileName <- The name of the file that contains the overflow for a given slot // // The user ID overflows are put into multiple files as an optimization because we are doing // sequential searching. //------------------------------------------------------------------------------------------------ void CAuthFileBase::PWRecToOverflowFileName( PWFileEntry *inPasswordRec, char *outFileName ) { if ( outFileName == NULL ) return; strcpy( outFileName, "authserveroverflow.exception" ); if ( inPasswordRec == NULL ) return; long simpleHash = ( (inPasswordRec->time ^ inPasswordRec->rnd) * (inPasswordRec->sequenceNumber + inPasswordRec->slot) ) % 100; sprintf( outFileName, "authserveroverflow.%lu", simpleHash ); }