/* * Copyright (c) 2000-2001 Apple Computer, Inc. All Rights Reserved. * * The contents of this file constitute Original Code as defined in and are * subject to the Apple Public Source License Version 1.2 (the 'License'). * You may not use this file except in compliance with the License. Please obtain * a copy of the License at http://www.apple.com/publicsource and read it before * using this file. * * This 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. */ // // database - database session management // #include "xdatabase.h" #include "agentquery.h" #include "key.h" #include "server.h" #include "cfnotifier.h" #include "SecurityAgentClient.h" #include // for default owner ACLs // // The map of database common segments // Mutex Database::commonLock; Database::CommonMap Database::commons; // // Create a Database object from initial parameters (create operation) // Database::Database(const DLDbIdentifier &id, const DBParameters ¶ms, Process &proc, const AccessCredentials *cred, const AclEntryPrototype *owner) : SecurityServerAcl(dbAcl, CssmAllocator::standard()), process(proc), mValidData(false), version(0), mBlob(NULL) { // save a copy of the credentials for later access control mCred = DataWalkers::copy(cred, CssmAllocator::standard()); // create a new random signature to complete the DLDbIdentifier Signature newSig; Server::active().random(newSig.bytes); DbIdentifier ident(id, newSig); // create common block and initialize common = new Common(ident); StLock _(*common); { StLock _(commonLock); assert(commons.find(ident) == commons.end()); // better be new! commons[ident] = common = new Common(ident); common->useCount++; } // new common is now visible but we hold its lock // obtain initial passphrase and generate keys common->mParams = params; common->setupKeys(cred); // establish initial ACL if (owner) cssmSetInitial(*owner); else cssmSetInitial(new AnyAclSubject()); mValidData = true; // for now, create the blob immediately //@@@ this could be deferred, at the cost of some additional //@@@ state monitoring. What happens if it locks before we have a blob? encode(); // register with process process.addDatabase(this); IFDEBUG(debug("SSdb", "database %s(%p) created, common at %p", common->dbName(), this, common)); IFDUMPING("SSdb", debugDump("creation complete")); } // // Create a Database object from a database blob (decoding) // Database::Database(const DLDbIdentifier &id, const DbBlob *blob, Process &proc, const AccessCredentials *cred) : SecurityServerAcl(dbAcl, CssmAllocator::standard()), process(proc), mValidData(false), version(0) { // perform basic validation on the incoming blob assert(blob); blob->validate(CSSMERR_APPLEDL_INVALID_DATABASE_BLOB); switch (blob->version) { #if defined(COMPAT_OSX_10_0) case blob->version_MacOS_10_0: break; #endif case blob->version_MacOS_10_1: break; default: CssmError::throwMe(CSSMERR_APPLEDL_INCOMPATIBLE_DATABASE_BLOB); } // save a copy of the credentials for later access control mCred = DataWalkers::copy(cred, CssmAllocator::standard()); // check to see if we already know about this database DbIdentifier ident(id, blob->randomSignature); StLock mapLock(commonLock); CommonMap::iterator it = commons.find(ident); if (it != commons.end()) { // already there common = it->second; // reuse common component //@@@ arbitrate sequence number here, perhaps update common->mParams StLock _(*common); // lock common against other users common->useCount++; IFDEBUG(debug("SSdb", "open database %s(%p) version %lx at known common %p(%d)", common->dbName(), this, blob->version, common, int(common->useCount))); } else { // newly introduced commons[ident] = common = new Common(ident); common->mParams = blob->params; common->useCount++; IFDEBUG(debug("SSdb", "open database %s(%p) version %lx with new common %p", common->dbName(), this, blob->version, common)); } // register with process process.addDatabase(this); mBlob = blob->copy(); IFDUMPING("SSdb", debugDump("end of decode")); } // // Destroy a Database // Database::~Database() { IFDEBUG(debug("SSdb", "deleting database %s(%p) common %p (%d refs)", common->dbName(), this, common, int(common->useCount))); IFDUMPING("SSdb", debugDump("deleting database instance")); process.removeDatabase(this); CssmAllocator::standard().free(mCred); // take the commonLock to avoid races against re-use of the common StLock __(commonLock); if (--common->useCount == 0 && common->isLocked()) { // last use of this database, and it's locked - discard IFDUMPING("SSdb", debugDump("discarding common")); discard(common); } else if (common->useCount == 0) IFDUMPING("SSdb", debugDump("retained because it's unlocked")); } // // (Re-)Authenticate the database. This changes the stored credentials. // void Database::authenticate(const AccessCredentials *cred) { StLock _(*common); CssmAllocator::standard().free(mCred); mCred = DataWalkers::copy(cred, CssmAllocator::standard()); } // // Encode the current database as a blob. // Note that this returns memory we own and keep. // DbBlob *Database::encode() { StLock _(*common); if (!validBlob()) { // unlock the database makeUnlocked(); // create new up-to-date blob DbBlob *blob = common->encode(*this); CssmAllocator::standard().free(mBlob); mBlob = blob; version = common->version; debug("SSdb", "encoded database %p(%s) version %ld", this, dbName(), version); } activity(); assert(mBlob); return mBlob; } // // Change the passphrase on a database // void Database::changePassphrase(const AccessCredentials *cred) { StLock _(*common); if (isLocked()) { CssmAutoData passphrase(CssmAllocator::standard(CssmAllocator::sensitive)); if (getBatchPassphrase(cred, CSSM_SAMPLE_TYPE_KEYCHAIN_LOCK, passphrase)) { // incoming sample contained data for unlock makeUnlocked(passphrase); } else { // perform standard unlock makeUnlocked(); } } else if (!mValidData) // need to decode to get our ACLs, passphrase available decode(common->passphrase); // get the new passphrase // @@@ unstaged version -- revise to filter passphrases QueryNewPassphrase query(*common, SecurityAgent::changePassphrase); query(cred, common->passphrase); common->version++; // blob state changed IFDEBUG(debug("SSdb", "Database %s(%p) passphrase changed", common->dbName(), this)); // send out a notification KeychainNotifier::passphraseChanged(identifier()); // I guess this counts as an activity activity(); } // // Unlock this database (if needed) by obtaining the passphrase in some // suitable way and then proceeding to unlock with it. Performs retries // where appropriate. Does absolutely nothing if the database is already unlocked. // void Database::unlock() { StLock _(*common); makeUnlocked(); } void Database::makeUnlocked() { IFDUMPING("SSdb", debugDump("default procedures unlock")); if (isLocked()) { assert(mBlob || (mValidData && common->passphrase)); QueryUnlock query(*this); query(mCred); if (isLocked()) // still locked, unlock failed CssmError::throwMe(CSSM_ERRCODE_OPERATION_AUTH_DENIED); // successfully unlocked activity(); // set timeout timer } else if (!mValidData) // need to decode to get our ACLs, passphrase available decode(common->passphrase); } // // Perform programmatic unlock of a database, given a passphrase. // void Database::unlock(const CssmData &passphrase) { StLock _(*common); makeUnlocked(passphrase); } void Database::makeUnlocked(const CssmData &passphrase) { if (isLocked()) { if (decode(passphrase)) return; else CssmError::throwMe(CSSM_ERRCODE_OPERATION_AUTH_DENIED); } else if (!mValidData) decode(common->passphrase); } // // Perform an actual unlock operation given a passphrase. // Caller must hold common lock. // bool Database::decode(const CssmData &passphrase) { if (mValidData && common->passphrase) { // just check return common->unlock(passphrase); } else { // decode our blob assert(mBlob); void *privateAclBlob; if (common->unlock(mBlob, passphrase, &privateAclBlob)) { if (!mValidData) { importBlob(mBlob->publicAclBlob(), privateAclBlob); mValidData = true; } CssmAllocator::standard().free(privateAclBlob); return true; } } return false; } // // Lock this database // void Database::lock() { common->lock(); } // // Lock all databases we know of. // This is an interim stop-gap measure, until we can work out how database // state should interact with true multi-session operation. // void Database::lockAllDatabases(bool forSleep) { StLock _(commonLock); // hold all changes to Common map debug("SSdb", "locking all %d known databases", int(commons.size())); for (CommonMap::iterator it = commons.begin(); it != commons.end(); it++) it->second->lock(true, forSleep); // lock, already holding commonLock } // // Given a Key for this database, encode it into a blob and return it. // KeyBlob *Database::encodeKey(const CssmKey &key, const CssmData &pubAcl, const CssmData &privAcl) { makeUnlocked(); // tell the cryptocore to form the key blob return common->encodeKeyCore(key, pubAcl, privAcl); } // // Given a "blobbed" key for this database, decode it into its real // key object and (re)populate its ACL. // void Database::decodeKey(KeyBlob *blob, CssmKey &key, void * &pubAcl, void * &privAcl) { makeUnlocked(); // we need our keys common->decodeKeyCore(blob, key, pubAcl, privAcl); // memory protocol: pubAcl points into blob; privAcl was allocated activity(); } // // Modify database parameters // void Database::setParameters(const DBParameters ¶ms) { StLock _(*common); makeUnlocked(); common->mParams = params; common->version++; // invalidate old blobs activity(); } // // Retrieve database parameters // void Database::getParameters(DBParameters ¶ms) { StLock _(*common); makeUnlocked(); params = common->mParams; //activity(); // getting parameters does not reset the idle timer } // // Intercept ACL change requests and reset blob validity // void Database::instantiateAcl() { StLock _(*common); makeUnlocked(); } void Database::noticeAclChange() { StLock _(*common); version = 0; } const Database *Database::relatedDatabase() const { return this; } // // Debugging support // #if defined(DEBUGDUMP) void Database::debugDump(const char *msg) { assert(common); const Signature &sig = common->identifier(); uint32 sig4; memcpy(&sig4, sig.bytes, sizeof(sig4)); Debug::dump("** %s(%8.8lx) common=%p(%ld) %s\n", common->dbName(), sig4, common, common->useCount, msg); if (isLocked()) Debug::dump(" locked"); else { Time::Absolute when = common->when(); time_t whenTime = time_t(when); Debug::dump(" UNLOCKED(%24.24s/%.2g)", ctime(&whenTime), (when - Time::now()).seconds()); } Debug::dump(" %s blobversion=%ld/%ld %svalidData", (common->isValid() ? "validkeys" : "!validkeys"), version, common->version, (mValidData ? "" : "!")); Debug::dump(" Params=(%ld %d)\n", common->mParams.idleTimeout, common->mParams.lockOnSleep); } #endif //DEBUGDUMP // // Database::Common basic features // Database::Common::Common(const DbIdentifier &id) : mIdentifier(id), sequence(0), passphrase(CssmAllocator::standard(CssmAllocator::sensitive)), useCount(0), version(1), mIsLocked(true) { } Database::Common::~Common() { // explicitly unschedule ourselves Server::active().clearTimer(this); } void Database::discard(Common *common) { // LOCKING: commonLock held, *common NOT held debug("SSdb", "discarding dbcommon %p (no users, locked)", common); commons.erase(common->identifier()); delete common; } bool Database::Common::unlock(DbBlob *blob, const CssmData &passphrase, void **privateAclBlob) { try { // Tell the cryptocore to (try to) decode itself. This will fail // in an astonishing variety of ways if the passphrase is wrong. decodeCore(blob, passphrase, privateAclBlob); } catch (...) { //@@@ which errors should we let through? Any? return false; } // save the passphrase (we'll need it for database encoding) this->passphrase = passphrase; // retrieve some public arguments mParams = blob->params; // now successfully unlocked mIsLocked = false; // set timeout activity(); // broadcast unlock notification KeychainNotifier::unlock(identifier()); return true; } // // Fast-path unlock: secrets already valid; just check passphrase and approve. // bool Database::Common::unlock(const CssmData &passphrase) { assert(isValid()); if (isLocked()) { if (passphrase == this->passphrase) { mIsLocked = false; KeychainNotifier::unlock(identifier()); return true; // okay } else return false; // failed } else return true; // was unlocked; no problem } void Database::Common::lock(bool holdingCommonLock, bool forSleep) { StLock locker(*this); if (!isLocked()) { if (forSleep && !mParams.lockOnSleep) return; // it doesn't want to //@@@ discard secrets here? That would make fast-path impossible. mIsLocked = true; KeychainNotifier::lock(identifier()); // if no database refers to us now, we're history StLock _(commonLock, false); if (!holdingCommonLock) _.lock(); if (useCount == 0) { locker.unlock(); // release object lock discard(this); } } } DbBlob *Database::Common::encode(Database &db) { assert(!isLocked()); // must have been unlocked by caller // export database ACL to blob form CssmData pubAcl, privAcl; db.exportBlob(pubAcl, privAcl); // tell the cryptocore to form the blob DbBlob form; form.randomSignature = identifier(); form.sequence = sequence; form.params = mParams; DbBlob *blob = encodeCore(form, passphrase, pubAcl, privAcl); // clean up and go db.allocator.free(pubAcl); db.allocator.free(privAcl); return blob; } // // Initialize a (new) database's key information. // This acquires the passphrase in the appropriate way. // When (successfully) done, the database is in the unlocked state. // void Database::Common::setupKeys(const AccessCredentials *cred) { // get the new passphrase // @@@ Un-staged version of the API - revise with acceptability tests QueryNewPassphrase query(*this, SecurityAgent::newDatabase); query(cred, passphrase); // we have the passphrase now generateNewSecrets(); // we're unlocked now mIsLocked = false; activity(); } // // Perform deferred lock processing for a database. // void Database::Common::action() { IFDEBUG(debug("SSdb", "common %s(%p) locked by timer (%d refs)", dbName(), this, int(useCount))); lock(); } void Database::Common::activity() { if (!isLocked()) Server::active().setTimer(this, int(mParams.idleTimeout)); }