+void KeychainImpl::tickle() {
+ if(!mSuppressTickle) {
+ globals().storageManager.tickleKeychain(this);
+ }
+}
+
+
+bool KeychainImpl::performKeychainUpgradeIfNeeded() {
+ // Grab this keychain's mutex.
+ StLock<Mutex>_(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 <rdar://problem/23950408> 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<Mutex>_(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());
+}
+