X-Git-Url: https://git.saurik.com/apple/security.git/blobdiff_plain/5c19dc3ae3bd8e40a9c028b0deddd50ff337692c..07691282a056c4efea71e1e505527601e8cc166b:/OSX/libsecurity_keychain/lib/Keychains.cpp diff --git a/OSX/libsecurity_keychain/lib/Keychains.cpp b/OSX/libsecurity_keychain/lib/Keychains.cpp index 1d94dee2..2b125fa8 100644 --- a/OSX/libsecurity_keychain/lib/Keychains.cpp +++ b/OSX/libsecurity_keychain/lib/Keychains.cpp @@ -37,14 +37,18 @@ #include #include #include +#include #include +#include +#include #include "SecKeychainPriv.h" #include #include -#include "DLDbListCFPref.h" +#include "DLDBListCFPref.h" #include +#include #include #include #include @@ -246,7 +250,7 @@ KeychainSchemaImpl::getAttributeInfoForRecordType(CSSM_DB_RECORDTYPE recordType, capacity *= 2; if (capacity <= i) capacity = i + 1; tagBuf=reinterpret_cast(realloc(tagBuf, (capacity*sizeof(UInt32)))); - formatBuf=reinterpret_cast(realloc(tagBuf, (capacity*sizeof(UInt32)))); + formatBuf=reinterpret_cast(realloc(formatBuf, (capacity*sizeof(UInt32)))); } tagBuf[i]=rit->first; formatBuf[i++]=rit->second; @@ -393,7 +397,8 @@ static void check_system_keychain() // KeychainImpl // KeychainImpl::KeychainImpl(const Db &db) - : mInCache(false), mDb(db), mCustomUnlockCreds (this), mIsInBatchMode (false), mMutex(Mutex::recursive) +: mCacheTimer(NULL), mSuppressTickle(false), mAttemptedUpgrade(false), mDbItemMapMutex(Mutex::recursive), mDbDeletedItemMapMutex(Mutex::recursive), + mInCache(false), mDb(db), mCustomUnlockCreds (this), mIsInBatchMode (false), mMutex(Mutex::recursive) { dispatch_once(&SecKeychainSystemKeychainChecked, ^{ check_system_keychain(); @@ -417,7 +422,7 @@ KeychainImpl::~KeychainImpl() } Mutex* -KeychainImpl::getMutexForObject() +KeychainImpl::getMutexForObject() const { return globals().storageManager.getStorageManagerMutex(); } @@ -481,6 +486,9 @@ KeychainImpl::create(UInt32 passwordLength, const void *inPassword) AclFactory::PasswordChangeCredentials pCreds (password, alloc); AclFactory::AnyResourceContext rcc(pCreds); create(&rcc); + + // Now that we've created, trigger setting the defaultCredentials + mDb->open(); } void KeychainImpl::create(ConstStringPtr inPassword) @@ -501,6 +509,9 @@ KeychainImpl::create() AclFactory aclFactory; AclFactory::AnyResourceContext rcc(aclFactory.unlockCred()); create(&rcc); + + // Now that we've created, trigger setting the defaultCredentials + mDb->open(); } void KeychainImpl::createWithBlob(CssmData &blob) @@ -645,11 +656,14 @@ KeychainImpl::changePassphrase(UInt32 oldPasswordLength, const void *oldPassword UInt32 newPasswordLength, const void *newPassword) { StLock_(mMutex); - + + secnotice("KCspi", "Attempting to change passphrase for %s", mDb->name()); + bool isSmartcard = (mDb->dl()->guid() == gGuidAppleSdCSPDL); TrackingAllocator allocator(Allocator::standard()); AutoCredentials cred = AutoCredentials(allocator); + if (oldPassword) { const CssmData &oldPass = *new(allocator) CssmData(const_cast(oldPassword), oldPasswordLength); @@ -722,6 +736,8 @@ KeychainImpl::authenticate(const CSSM_ACCESS_CREDENTIALS *cred) UInt32 KeychainImpl::status() const { + StLock_(mMutex); + // @@@ We should figure out the read/write status though a DL passthrough // or some other way. Also should locked be unlocked read only or just // read-only? @@ -758,34 +774,32 @@ KeychainImpl::isActive() const void KeychainImpl::completeAdd(Item &inItem, PrimaryKey &primaryKey) { - - // The inItem shouldn't be in the cache yet assert(!inItem->inCache()); // Insert inItem into mDbItemMap with key primaryKey. p.second will be // true if it got inserted. If not p.second will be false and p.first // will point to the current entry with key primaryKey. + StLock _(mDbItemMapMutex); pair p = mDbItemMap.insert(DbItemMap::value_type(primaryKey, inItem.get())); if (!p.second) { // There was already an ItemImpl * in mDbItemMap with key - // primaryKey. Get a ref to the pointer to it so we can assign a - // new value to it below. + // primaryKey. Remove it, and try the add again. ItemImpl *oldItem = p.first->second; - // @@@ If this happens we are breaking our API contract of + // @@@ If this happens we are breaking our API contract of // uniquifying items. We really need to insert the item into the // map before we start the add. And have the item be in an // "is being added" state. - assert(oldItem->inCache()); - secdebug("keychain", "add of new item %p somehow replaced %p", + secnotice("keychain", "add of new item %p somehow replaced %p", inItem.get(), oldItem); - - // make sure that we both mark the item and remove the item from the cache - removeItem(oldItem->primaryKey(), oldItem); - oldItem = inItem.get(); + + mDbItemMap.erase(p.first); + oldItem->inCache(false); + forceRemoveFromCache(oldItem); + mDbItemMap.insert(DbItemMap::value_type(primaryKey, inItem.get())); } inItem->inCache(true); @@ -794,6 +808,8 @@ void KeychainImpl::completeAdd(Item &inItem, PrimaryKey &primaryKey) void KeychainImpl::addCopy(Item &inItem) { + StLock_(mMutex); + Keychain keychain(this); PrimaryKey primaryKey = inItem->addWithCopyInfo(keychain, true); completeAdd(inItem, primaryKey); @@ -803,6 +819,8 @@ KeychainImpl::addCopy(Item &inItem) void KeychainImpl::add(Item &inItem) { + StLock_(mMutex); + Keychain keychain(this); PrimaryKey primaryKey = inItem->add(keychain); completeAdd(inItem, primaryKey); @@ -820,6 +838,7 @@ KeychainImpl::didUpdate(const Item &inItem, PrimaryKey &oldPK, assert(inItem->inCache()); if (inItem->inCache()) { + StLock _(mDbItemMapMutex); // First remove the entry for inItem in mDbItemMap with key oldPK. DbItemMap::iterator it = mDbItemMap.find(oldPK); if (it != mDbItemMap.end() && (ItemImpl*) it->second == inItem.get()) @@ -833,47 +852,62 @@ KeychainImpl::didUpdate(const Item &inItem, PrimaryKey &oldPK, if (!p.second) { // There was already an ItemImpl * in mDbItemMap with key - // primaryKey. Get a ref to the pointer to it so we can assign - // a new value to it below. + // primaryKey. Remove it, and try the add again. ItemImpl *oldItem = p.first->second; - // @@@ If this happens we are breaking our API contract of + // @@@ If this happens we are breaking our API contract of // uniquifying items. We really need to insert the item into // the map with the new primary key before we start the update. // And have the item be in an "is being updated" state. - assert(oldItem->inCache()); - secdebug("keychain", "update of item %p somehow replaced %p", + secnotice("keychain", "update of item %p somehow replaced %p", inItem.get(), oldItem); - oldItem->inCache(false); - oldItem = inItem.get(); + + mDbItemMap.erase(p.first); + oldItem->inCache(false); + forceRemoveFromCache(oldItem); + mDbItemMap.insert(DbItemMap::value_type(newPK, inItem.get())); } } } - - postEvent(kSecUpdateEvent, inItem); + + // Item updates now are technically a delete and re-add, so post these events instead of kSecUpdateEvent + postEvent(kSecDeleteEvent, inItem, oldPK); + postEvent(kSecAddEvent, inItem); } void KeychainImpl::deleteItem(Item &inoutItem) { + StLock_(mMutex); + { - // We don't need to hold the DO mutex through event posting, and, in fact, doing so causes deadlock. - // Hold it only as long as needed, instead. - - // item must be persistent if (!inoutItem->isPersistent()) MacOSError::throwMe(errSecInvalidItemRef); + secinfo("kcnotify", "starting deletion of item %p", inoutItem.get()); + DbUniqueRecord uniqueId = inoutItem->dbUniqueRecord(); PrimaryKey primaryKey = inoutItem->primaryKey(); uniqueId->deleteRecord(); - // Don't remove the item from the mDbItemMap here since this would cause - // us to report a new item to our caller when we receive the - // kSecDeleteEvent notification. - // It will be removed before we post the notification, because - // CCallbackMgr will call didDeleteItem() + // Move the item from mDbItemMap to mDbDeletedItemMap. We need the item + // to give to the client process when we receive the kSecDeleteEvent + // notification, but if that notification never arrives, we don't want + // the item hanging around. When didDeleteItem is called by CCallbackMgr, + // we'll remove all traces of the item. + + if (inoutItem->inCache()) { + StLock _(mDbItemMapMutex); + StLock __(mDbDeletedItemMapMutex); + // Only look for it if it's in the cache + DbItemMap::iterator it = mDbItemMap.find(primaryKey); + + if (it != mDbItemMap.end() && (ItemImpl*) it->second == inoutItem.get()) { + mDbDeletedItemMap.insert(DbItemMap::value_type(primaryKey, it->second)); + mDbItemMap.erase(it); + } + } // Post the notification for the item deletion with // the primaryKey obtained when the item still existed @@ -882,13 +916,20 @@ KeychainImpl::deleteItem(Item &inoutItem) postEvent(kSecDeleteEvent, inoutItem); } +void KeychainImpl::changeDatabase(CssmClient::Db db) +{ + StLock_(mDbMutex); + mDb = db; + mDb->defaultCredentials(this); +} + CssmClient::CSP KeychainImpl::csp() { StLock_(mMutex); - if (!mDb->dl()->subserviceMask() & CSSM_SERVICE_CSP) + if (!(mDb->dl()->subserviceMask() & CSSM_SERVICE_CSP)) MacOSError::throwMe(errSecInvalidKeychain); // Try to cast first to a CSPDL to handle case where we don't have an SSDb @@ -922,6 +963,26 @@ KeychainImpl::makePrimaryKey(CSSM_DB_RECORDTYPE recordType, DbUniqueRecord &uniq return PrimaryKey(primaryKeyAttrs); } +PrimaryKey +KeychainImpl::makePrimaryKey(CSSM_DB_RECORDTYPE recordType, DbAttributes* currentAttributes) +{ + StLock_(mMutex); + + DbAttributes primaryKeyAttrs; + primaryKeyAttrs.recordType(recordType); + gatherPrimaryKeyAttributes(primaryKeyAttrs); + + for(int i = 0; i < primaryKeyAttrs.size(); i++) { + CssmDbAttributeData& attr = primaryKeyAttrs[i]; + + CssmDbAttributeData * actual = currentAttributes->find(attr.info()); + if(actual) { + attr.set(*actual, Allocator::standard()); + } + } + return PrimaryKey(primaryKeyAttrs); +} + const CssmAutoDbRecordAttributeInfo & KeychainImpl::primaryKeyInfosFor(CSSM_DB_RECORDTYPE recordType) { @@ -947,7 +1008,7 @@ KeychainImpl::primaryKeyInfosFor(CSSM_DB_RECORDTYPE recordType) void KeychainImpl::gatherPrimaryKeyAttributes(DbAttributes& primaryKeyAttrs) { - StLock_(mMutex); + StLock _(mMutex); const CssmAutoDbRecordAttributeInfo &infos = primaryKeyInfosFor(primaryKeyAttrs.recordType()); @@ -960,23 +1021,29 @@ void KeychainImpl::gatherPrimaryKeyAttributes(DbAttributes& primaryKeyAttrs) ItemImpl * KeychainImpl::_lookupItem(const PrimaryKey &primaryKey) { + StLock _(mDbItemMapMutex); DbItemMap::iterator it = mDbItemMap.find(primaryKey); if (it != mDbItemMap.end()) { - if (it->second == NULL) - { - // we've been weak released... - mDbItemMap.erase(it); - } - else - { - return it->second; - } + return it->second; } return NULL; } +ItemImpl * +KeychainImpl::_lookupDeletedItemOnly(const PrimaryKey &primaryKey) +{ + StLock _(mDbDeletedItemMapMutex); + DbItemMap::iterator it = mDbDeletedItemMap.find(primaryKey); + if (it != mDbDeletedItemMap.end()) + { + return it->second; + } + + return NULL; +} + Item KeychainImpl::item(const PrimaryKey &primaryKey) { @@ -984,14 +1051,14 @@ KeychainImpl::item(const PrimaryKey &primaryKey) // Lookup the item in the map while holding the apiLock. ItemImpl *itemImpl = _lookupItem(primaryKey); - if (itemImpl) + if (itemImpl) { return Item(itemImpl); + } try { // We didn't find it so create a new item with just a keychain and - // a primary key. However since we aren't holding - // globals().apiLock anymore some other thread might have beaten + // a primary key. Some other thread might have beaten // us to creating this item and adding it to the cache. If that // happens we retry the lookup. return Item(this, primaryKey); @@ -1010,6 +1077,18 @@ KeychainImpl::item(const PrimaryKey &primaryKey) throw; } } +// Check for an item that may have been deleted. +Item +KeychainImpl::itemdeleted(const PrimaryKey& primaryKey) { + StLock_(mMutex); + + Item i = _lookupDeletedItemOnly(primaryKey); + if(i.get()) { + return i; + } else { + return item(primaryKey); + } +} Item @@ -1081,6 +1160,7 @@ KeychainImpl::addItem(const PrimaryKey &primaryKey, ItemImpl *dbItemImpl) // Insert dbItemImpl into mDbItemMap with key primaryKey. p.second will // be true if it got inserted. If not p.second will be false and p.first // will point to the current entry with key primaryKey. + StLock __(mDbItemMapMutex); pair p = mDbItemMap.insert(DbItemMap::value_type(primaryKey, dbItemImpl)); @@ -1102,7 +1182,7 @@ KeychainImpl::didDeleteItem(ItemImpl *inItemImpl) StLock_(mMutex); // Called by CCallbackMgr - secdebug("kcnotify", "%p notified that item %p was deleted", this, inItemImpl); + secinfo("kcnotify", "%p notified that item %p was deleted", this, inItemImpl); removeItem(inItemImpl->primaryKey(), inItemImpl); } @@ -1115,13 +1195,66 @@ KeychainImpl::removeItem(const PrimaryKey &primaryKey, ItemImpl *inItemImpl) if (!inItemImpl->inCache()) return; - DbItemMap::iterator it = mDbItemMap.find(primaryKey); - if (it != mDbItemMap.end() && (ItemImpl*) it->second == inItemImpl) - mDbItemMap.erase(it); + { + StLock _(mDbItemMapMutex); + DbItemMap::iterator it = mDbItemMap.find(primaryKey); + if (it != mDbItemMap.end() && (ItemImpl*) it->second == inItemImpl) { + mDbItemMap.erase(it); + } + } // drop mDbItemMapMutex + + { + StLock _(mDbDeletedItemMapMutex); + DbItemMap::iterator it = mDbDeletedItemMap.find(primaryKey); + if (it != mDbDeletedItemMap.end() && (ItemImpl*) it->second == inItemImpl) { + mDbDeletedItemMap.erase(it); + } + } // drop mDbDeletedItemMapMutex inItemImpl->inCache(false); } +void +KeychainImpl::forceRemoveFromCache(ItemImpl* inItemImpl) { + try { + // Wrap all this in a try-block and ignore all errors - we're trying to clean up these maps + { + StLock _(mDbItemMapMutex); + for(DbItemMap::iterator it = mDbItemMap.begin(); it != mDbItemMap.end(); ) { + if(it->second == inItemImpl) { + // Increment the iterator, but use its pre-increment value for the erase + it->second->inCache(false); + mDbItemMap.erase(it++); + } else { + it++; + } + } + } // drop mDbItemMapMutex + + { + StLock _(mDbDeletedItemMapMutex); + for(DbItemMap::iterator it = mDbDeletedItemMap.begin(); it != mDbDeletedItemMap.end(); ) { + if(it->second == inItemImpl) { + // Increment the iterator, but use its pre-increment value for the erase + it->second->inCache(false); + mDbDeletedItemMap.erase(it++); + } else { + it++; + } + } + } // drop mDbDeletedItemMapMutex + } catch(UnixError ue) { + secnotice("keychain", "caught UnixError: %d %s", ue.unixError(), ue.what()); + } catch (CssmError cssme) { + const char* errStr = cssmErrorString(cssme.error); + secnotice("keychain", "caught CssmError: %d %s", (int) cssme.error, errStr); + } catch (MacOSError mose) { + secnotice("keychain", "MacOSError: %d", (int)mose.osStatus()); + } catch(...) { + secnotice("keychain", "Unknown error"); + } +} + void KeychainImpl::getAttributeInfoForItemID(CSSM_DB_RECORDTYPE itemID, SecKeychainAttributeInfo **Info) @@ -1222,21 +1355,28 @@ KeychainImpl::setBatchMode(Boolean mode, Boolean rollback) } // notify that a keychain has changed in too many ways to count - KCEventNotifier::PostKeychainEvent(kSecKeychainLeftBatchModeEvent); + KCEventNotifier::PostKeychainEvent((SecKeychainEvent) kSecKeychainLeftBatchModeEvent); mEventBuffer->clear(); } else { - KCEventNotifier::PostKeychainEvent(kSecKeychainEnteredBatchModeEvent); + KCEventNotifier::PostKeychainEvent((SecKeychainEvent) kSecKeychainEnteredBatchModeEvent); } } - void KeychainImpl::postEvent(SecKeychainEvent kcEvent, ItemImpl* item) +{ + postEvent(kcEvent, item, NULL); +} + +void +KeychainImpl::postEvent(SecKeychainEvent kcEvent, ItemImpl* item, PrimaryKey pk) { PrimaryKey primaryKey; - { + if(pk.get()) { + primaryKey = pk; + } else { StLock_(mMutex); if (item != NULL) @@ -1264,6 +1404,422 @@ KeychainImpl::postEvent(SecKeychainEvent kcEvent, ItemImpl* item) } } +void KeychainImpl::tickle() { + if(!mSuppressTickle) { + globals().storageManager.tickleKeychain(this); + } +} + + +bool KeychainImpl::performKeychainUpgradeIfNeeded() { + // Grab this keychain's mutex. + StLock_(mMutex); + + if(!globals().integrityProtection()) { + secnotice("integrity", "skipping upgrade for %s due to global integrity protection being disabled", mDb->name()); + return false; + } + + // We need a CSP database for 'upgrade' to be meaningful + if((mDb->dl()->subserviceMask() & CSSM_SERVICE_CSP) == 0) { + return false; + } + + // We only want to upgrade file-based Apple keychains. Check the GUID. + if(mDb->dl()->guid() != gGuidAppleCSPDL) { + secinfo("integrity", "skipping upgrade for %s due to guid mismatch\n", mDb->name()); + return false; + } + + // If we've already attempted an upgrade on this keychain, don't bother again + if(mAttemptedUpgrade) { + return false; + } + + // Don't upgrade the System root certificate keychain (to make old tp code happy) + if(strncmp(mDb->name(), SYSTEM_ROOT_STORE_PATH, strlen(SYSTEM_ROOT_STORE_PATH)) == 0) { + secinfo("integrity", "skipping upgrade for %s\n", mDb->name()); + return false; + } + + uint32 dbBlobVersion = SecurityServer::DbBlob::version_MacOS_10_0; + + try { + dbBlobVersion = mDb->dbBlobVersion(); + } catch (CssmError cssme) { + if(cssme.error == CSSMERR_DL_DATASTORE_DOESNOT_EXIST) { + // oh well! We tried to get the blob version of a database + // that doesn't exist. It doesn't need migration, so do nothing. + secnotice("integrity", "dbBlobVersion() failed for a non-existent database"); + return false; + } else { + // Some other error occurred. We can't upgrade this keychain, so fail. + const char* errStr = cssmErrorString(cssme.error); + secnotice("integrity", "dbBlobVersion() failed for a CssmError: %d %s", (int) cssme.error, errStr); + return false; + } + } catch (...) { + secnotice("integrity", "dbBlobVersion() failed for an unknown reason"); + return false; + } + + + + // Check the location of this keychain + string path = mDb->name(); + string keychainDbPath = StorageManager::makeKeychainDbFilename(path); + + bool inHomeLibraryKeychains = StorageManager::pathInHomeLibraryKeychains(path); + + string keychainDbSuffix = "-db"; + bool endsWithKeychainDb = (path.size() > keychainDbSuffix.size() && (0 == path.compare(path.size() - keychainDbSuffix.size(), keychainDbSuffix.size(), keychainDbSuffix))); + + bool isSystemKeychain = (0 == path.compare("/Library/Keychains/System.keychain")); + + bool result = false; + + if(inHomeLibraryKeychains && endsWithKeychainDb && dbBlobVersion == SecurityServer::DbBlob::version_MacOS_10_0) { + // something has gone horribly wrong: an old-versioned keychain has a .keychain-db name. Rename it. + string basePath = path; + basePath.erase(basePath.end()-3, basePath.end()); + + attemptKeychainRename(path, basePath, dbBlobVersion); + + // If we moved to a good path, we might still want to perform the upgrade. Update our variables. + path = mDb->name(); + + try { + dbBlobVersion = mDb->dbBlobVersion(); + } catch (CssmError cssme) { + const char* errStr = cssmErrorString(cssme.error); + secnotice("integrity", "dbBlobVersion() after a rename failed for a CssmError: %d %s", (int) cssme.error, errStr); + return false; + } catch (...) { + secnotice("integrity", "dbBlobVersion() failed for an unknown reason after a rename"); + return false; + } + + endsWithKeychainDb = (path.size() > keychainDbSuffix.size() && (0 == path.compare(path.size() - keychainDbSuffix.size(), keychainDbSuffix.size(), keychainDbSuffix))); + keychainDbPath = StorageManager::makeKeychainDbFilename(path); + secnotice("integrity", "after rename, our database thinks that it is %s", path.c_str()); + } + + // Migrate an old keychain in ~/Library/Keychains + if(inHomeLibraryKeychains && dbBlobVersion != SecurityServer::DbBlob::version_partition && !endsWithKeychainDb) { + // We can only attempt to migrate an unlocked keychain. + if(mDb->isLocked()) { + // However, it's possible that while we weren't doing any keychain operations, someone upgraded the keychain, + // and then locked it. No way around hitting the filesystem here: check for the existence of a new file and, + // if no new file exists, quit. + DLDbIdentifier mungedDLDbIdentifier = StorageManager::mungeDLDbIdentifier(mDb->dlDbIdentifier(), false); + string mungedPath(mungedDLDbIdentifier.dbName()); + + // If this matches the file we already have, skip the upgrade. Otherwise, continue. + if(mungedPath == path) { + secnotice("integrity", "skipping upgrade for locked keychain %s\n", mDb->name()); + return false; + } + } + + result = keychainMigration(path, dbBlobVersion, keychainDbPath, SecurityServer::DbBlob::version_partition); + } else if(inHomeLibraryKeychains && dbBlobVersion == SecurityServer::DbBlob::version_partition && !endsWithKeychainDb) { + // This is a new-style keychain with the wrong name, try to rename it + attemptKeychainRename(path, keychainDbPath, dbBlobVersion); + result = true; + } else if(isSystemKeychain && dbBlobVersion == SecurityServer::DbBlob::version_partition) { + // Try to "unupgrade" the system keychain, to clean up our old issues + secnotice("integrity", "attempting downgrade for %s version %d (%d %d %d)", path.c_str(), dbBlobVersion, inHomeLibraryKeychains, endsWithKeychainDb, isSystemKeychain); + + // First step: acquire the credentials to allow for ACL modification + SecurityServer::SystemKeychainKey skk(kSystemUnlockFile); + if(skk.valid()) { + // We've managed to read the key; now, create credentials using it + CssmClient::Key systemKeychainMasterKey(csp(), skk.key(), true); + CssmClient::AclFactory::MasterKeyUnlockCredentials creds(systemKeychainMasterKey, Allocator::standard(Allocator::sensitive)); + + // Attempt the downgrade, using our master key as the ACL override + result = keychainMigration(path, dbBlobVersion, path, SecurityServer::DbBlob::version_MacOS_10_0, creds.getAccessCredentials()); + } else { + secnotice("integrity", "Couldn't read System.keychain key, skipping update"); + } + } else { + secinfo("integrity", "not attempting migration for %s version %d (%d %d %d)", path.c_str(), dbBlobVersion, inHomeLibraryKeychains, endsWithKeychainDb, isSystemKeychain); + + // Since we don't believe any migration needs to be done here, mark the + // migration as "attempted" to short-circuit future checks. + mAttemptedUpgrade = true; + } + + // We might have changed our location on disk. Let StorageManager know. + globals().storageManager.registerKeychainImpl(this); + + // if we attempted a migration, try to clean up leftover files from XARA backup have provided me with 12GB of login keychain copies + if(result) { + string pattern = path + "_*_backup"; + glob_t pglob = {}; + secnotice("integrity", "globbing for %s", pattern.c_str()); + int globresult = glob(pattern.c_str(), GLOB_MARK, NULL, &pglob); + if(globresult == 0) { + secnotice("integrity", "glob: %lu results", pglob.gl_pathc); + if(pglob.gl_pathc > 10) { + // There are more than 10 backup files, indicating a problem. + // Delete all but one of them. Under rdar://23950408, they should all be identical. + secnotice("integrity", "saving backup file: %s", pglob.gl_pathv[0]); + for(int i = 1; i < pglob.gl_pathc; i++) { + secnotice("integrity", "cleaning up backup file: %s", pglob.gl_pathv[i]); + // ignore return code; this is a best-effort cleanup + unlink(pglob.gl_pathv[i]); + } + } + + struct stat st; + bool pathExists = (::stat(path.c_str(), &st) == 0); + bool keychainDbPathExists = (::stat(keychainDbPath.c_str(), &st) == 0); + + if(!pathExists && keychainDbPathExists && pglob.gl_pathc >= 1) { + // We have a file at keychainDbPath, no file at path, and at least one backup keychain file. + // + // Move the backup file to path, to simulate the current "split-world" view, + // which copies from path to keychainDbPath, then modifies keychainDbPath. + secnotice("integrity", "moving backup file %s to %s", pglob.gl_pathv[0], path.c_str()); + ::rename(pglob.gl_pathv[0], path.c_str()); + } + } + + globfree(&pglob); + } + + return result; +} + +bool KeychainImpl::keychainMigration(const string oldPath, const uint32 dbBlobVersion, const string newPath, const uint32 newBlobVersion, const AccessCredentials *cred) { + secnotice("integrity", "going to migrate %s at version %d to", oldPath.c_str(), dbBlobVersion); + secnotice("integrity", " %s at version %d", newPath.c_str(), newBlobVersion); + + // We need to opportunistically perform the upgrade/reload dance. + // + // If the keychain is unlocked, try to upgrade it. + // In either case, reload the database from disk. + + // Try to grab the keychain mutex (although we should already have it) + StLock_(mMutex); + + // Take the file lock on the existing database. We don't need to commit this txion, because we're not planning to + // change the original keychain. + FileLockTransaction fileLockmDb(mDb); + + // Let's reload this keychain to see if someone changed it on disk + globals().storageManager.reloadKeychain(this); + + bool result = false; + + try { + // We can only attempt an upgrade if the keychain is currently unlocked + // There's a TOCTTOU issue here, but it's going to be rare in practice, and the upgrade will simply fail. + if(!mDb->isLocked()) { + secnotice("integrity", "have a plan to migrate database %s", mDb->name()); + // Database blob is out of date. Attempt a migration. + uint32 convertedVersion = attemptKeychainMigration(oldPath, dbBlobVersion, newPath, newBlobVersion, cred); + if(convertedVersion == newBlobVersion) { + secnotice("integrity", "conversion succeeded"); + result = true; + } else { + secnotice("integrity", "conversion failed, keychain is still %d", convertedVersion); + } + } else { + secnotice("integrity", "keychain is locked, can't upgrade"); + } + } catch (CssmError cssme) { + const char* errStr = cssmErrorString(cssme.error); + secnotice("integrity", "caught CssmError: %d %s", (int) cssme.error, errStr); + } catch (...) { + // Something went wrong, but don't worry about it. + secnotice("integrity", "caught unknown error"); + } + + // No matter if the migrator succeeded, we need to reload this keychain from disk. + secnotice("integrity", "reloading keychain after migration"); + globals().storageManager.reloadKeychain(this); + secnotice("integrity", "database %s is now version %d", mDb->name(), mDb->dbBlobVersion()); + + return result; +} + +// Make sure you have this keychain's mutex and write lock when you call this function! +uint32 KeychainImpl::attemptKeychainMigration(const string oldPath, const uint32 oldBlobVersion, const string newPath, const uint32 newBlobVersion, const AccessCredentials* cred) { + if(mDb->dbBlobVersion() == newBlobVersion) { + // Someone else upgraded this, hurray! + secnotice("integrity", "reloaded keychain version %d, quitting", mDb->dbBlobVersion()); + return newBlobVersion; + } + + mAttemptedUpgrade = true; + uint32 newDbVersion = oldBlobVersion; + + if( (oldBlobVersion == SecurityServer::DbBlob::version_MacOS_10_0 && newBlobVersion == SecurityServer::DbBlob::version_partition) || + (oldBlobVersion == SecurityServer::DbBlob::version_partition && newBlobVersion == SecurityServer::DbBlob::version_MacOS_10_0 && cred != NULL)) { + // Here's the upgrade outline: + // + // 1. Make a copy of the keychain with the new file path + // 2. Open that keychain database. + // 3. Recode it to use the new version. + // 4. Notify the StorageManager that the DLDB identifier for this keychain has changed. + // + // If we're creating a new keychain file, on failure, try to delete the new file. Otherwise, + // everyone will try to use it. + + secnotice("integrity", "attempting migration from version %d to %d", oldBlobVersion, newBlobVersion); + + Db db; + bool newFile = (oldPath != newPath); + + try { + DLDbIdentifier dldbi(dlDbIdentifier().ssuid(), newPath.c_str(), dlDbIdentifier().dbLocation()); + if(newFile) { + secnotice("integrity", "creating a new keychain at %s", newPath.c_str()); + db = mDb->cloneTo(dldbi); + } else { + secnotice("integrity", "using old keychain at %s", newPath.c_str()); + db = mDb; + } + FileLockTransaction fileLockDb(db); + + if(newFile) { + // since we're creating a completely new file, if this migration fails, delete the new file + fileLockDb.setDeleteOnFailure(); + } + + // Let the upgrade begin. + newDbVersion = db->recodeDbToVersion(newBlobVersion); + if(newDbVersion != newBlobVersion) { + // Recoding failed. Don't proceed. + secnotice("integrity", "recodeDbToVersion failed, version is still %d", newDbVersion); + return newDbVersion; + } + + secnotice("integrity", "recoded db successfully, adding extra integrity"); + + Keychain keychain(db); + + // Breaking abstraction, but what're you going to do? + // Don't upgrade this keychain, since we just upgraded the DB + // But the DB won't return any new data until the txion commits + keychain->mAttemptedUpgrade = true; + keychain->mSuppressTickle = true; + + SecItemClass classes[] = {kSecGenericPasswordItemClass, + kSecInternetPasswordItemClass, + kSecPublicKeyItemClass, + kSecPrivateKeyItemClass, + kSecSymmetricKeyItemClass}; + + for(int i = 0; i < sizeof(classes) / sizeof(classes[0]); i++) { + Item item; + KCCursor kcc = keychain->createCursor(classes[i], NULL); + + // During recoding, we might have deleted some corrupt keys. + // Because of this, we might have zombie SSGroup records left in + // the database that have no matching key. Tell the KCCursor to + // delete these if found. + // This will also try to suppress any other invalid items. + kcc->setDeleteInvalidRecords(true); + + while(kcc->next(item)) { + try { + if(newBlobVersion == SecurityServer::DbBlob::version_partition) { + // Force the item to set integrity. The keychain is confused about its version because it hasn't written to disk yet, + // but if we've reached this point, the keychain supports integrity. + item->setIntegrity(true); + } else if(newBlobVersion == SecurityServer::DbBlob::version_MacOS_10_0) { + // We're downgrading this keychain. Pass in whatever credentials our caller thinks will allow this ACL modification. + item->removeIntegrity(cred); + } + } catch(CssmError cssme) { + // During recoding, we might have deleted some corrupt keys. Because of this, we might have zombie SSGroup records left in + // the database that have no matching key. If we get a DL_RECORD_NOT_FOUND error, delete the matching item record. + if (cssme.osStatus() == CSSMERR_DL_RECORD_NOT_FOUND) { + secnotice("integrity", "deleting corrupt (Not Found) record"); + keychain->deleteItem(item); + } else if(cssme.osStatus() == CSSMERR_CSP_INVALID_KEY) { + secnotice("integrity", "deleting corrupt key record"); + keychain->deleteItem(item); + } else { + throw; + } + } + } + } + + // Tell securityd we're done with the upgrade, to re-enable all protections + db->recodeFinished(); + + // If we reach here, tell the file locks to commit the transaction and return the new blob version + fileLockDb.success(); + + secnotice("integrity", "success, returning version %d", newDbVersion); + return newDbVersion; + } catch(UnixError ue) { + secnotice("integrity", "caught UnixError: %d %s", ue.unixError(), ue.what()); + } catch (CssmError cssme) { + const char* errStr = cssmErrorString(cssme.error); + secnotice("integrity", "caught CssmError: %d %s", (int) cssme.error, errStr); + } catch (MacOSError mose) { + secnotice("integrity", "MacOSError: %d", (int)mose.osStatus()); + } catch (const std::bad_cast & e) { + secnotice("integrity", "***** bad cast: %s", e.what()); + } catch (...) { + // We failed to migrate. We won't commit the transaction, so the blob on-disk stays the same. + secnotice("integrity", "***** unknown error"); + } + } else { + secnotice("integrity", "no migration path for %s at version %d to", oldPath.c_str(), oldBlobVersion); + secnotice("integrity", " %s at version %d", newPath.c_str(), newBlobVersion); + return oldBlobVersion; + } + + // If we reached here, the migration failed. Return the old version. + return oldBlobVersion; +} + +void KeychainImpl::attemptKeychainRename(const string oldPath, const string newPath, uint32 blobVersion) { + secnotice("integrity", "attempting to rename keychain (%d) from %s to %s", blobVersion, oldPath.c_str(), newPath.c_str()); + + // Take the file lock on this database, so other people won't try to move it before we do + // NOTE: during a migration from a v256 to a v512 keychain, the db is first copied from the .keychain to the + // .keychain-db path. Other non-migrating processes, if they open the keychain, enter this function to + // try to move it back. These will attempt to take the .keychain-db file lock, but they will not succeed + // until the migration is finished. Once they acquire that, they might try to take the .keychain file lock. + // This is technically lock inversion, but deadlocks will not happen since the migrating process creates the + // .keychain-db file lock before creating the .keychain-db file, so other processes will not try to grab the + // .keychain-db lock in this function before the migrating process already has it. + FileLockTransaction fileLockmDb(mDb); + + // first, check if someone renamed this keychain while we were grabbing the file lock + globals().storageManager.reloadKeychain(this); + + uint32 dbBlobVersion = SecurityServer::DbBlob::version_MacOS_10_0; + + try { + dbBlobVersion = mDb->dbBlobVersion(); + } catch (...) { + secnotice("integrity", "dbBlobVersion() failed for an unknown reason while renaming, aborting rename"); + return; + } + + if(dbBlobVersion != blobVersion) { + secnotice("integrity", "database version changed while we were grabbing the file lock; aborting rename"); + return; + } + + if(oldPath != mDb->name()) { + secnotice("integrity", "database location changed while we were grabbing the file lock; aborting rename"); + return; + } + + // we're still at the original location and version; go ahead and do the move + globals().storageManager.rename(this, newPath.c_str()); +} + Keychain::Keychain() { dispatch_once(&SecKeychainSystemKeychainChecked, ^{ @@ -1330,3 +1886,21 @@ bool KeychainImpl::mayDelete() { return true; } + +bool KeychainImpl::hasIntegrityProtection() { + StLock_(mMutex); + + // This keychain only supports integrity if there's a database attached, that database is an Apple CSPDL, and the blob version is high enough + if(mDb && (mDb->dl()->guid() == gGuidAppleCSPDL)) { + if(mDb->dbBlobVersion() >= SecurityServer::DbBlob::version_partition) { + return true; + } else { + secnotice("integrity", "keychain blob version does not support integrity"); + return false; + } + } else { + secnotice("integrity", "keychain guid does not support integrity"); + return false; + } +} +