agentquery.cpp   [plain text]


/*
 * Copyright (c) 2000-2004 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@
 */

//
// passphrases - canonical code to obtain passphrases
//
#include "agentquery.h"
#include "authority.h"

#include <Security/AuthorizationTags.h>
#include <Security/AuthorizationTagsPriv.h>

//
// NOSA support functions. This is a test mode where the SecurityAgent
// is simulated via stdio in the client. Good for running automated tests
// of client programs. Only available if -DNOSA when compiling.
//
#if defined(NOSA)

#include <cstdarg>

static void getNoSA(char *buffer, size_t bufferSize, const char *fmt, ...)
{
	// write prompt
	va_list args;
	va_start(args, fmt);
	vfprintf(stdout, fmt, args);
	va_end(args);

	// read reply
	memset(buffer, 0, bufferSize);
	const char *nosa = getenv("NOSA");
	if (!strcmp(nosa, "-")) {
		if (fgets(buffer, bufferSize-1, stdin) == NULL)
			CssmError::throwMe(CSSM_ERRCODE_NO_USER_INTERACTION);
		buffer[strlen(buffer)-1] = '\0';	// remove trailing newline
		if (!isatty(fileno(stdin)))
			printf("%s\n", buffer);			// echo to output if input not terminal
	} else {
		strncpy(buffer, nosa, bufferSize-1);
		printf("%s\n", buffer);
	}
	if (buffer[0] == '\0')				// empty input -> cancellation
		CssmError::throwMe(CSSM_ERRCODE_USER_CANCELED);
}

#endif //NOSA


using SecurityAgent::Reason;
using namespace Authorization;

SecurityAgentQuery::SecurityAgentQuery(const AuthHostType type, Session &session) : mAuthHostType(type), mHostInstance(session.authhost(mAuthHostType)), mConnection(&Server::connection())
{
	// this may take a while
	Server::active().longTermActivity();
    secdebug("SecurityAgentQuery", "new SecurityAgentQuery(%p)", this);
}

SecurityAgentQuery::~SecurityAgentQuery()
{
    secdebug("SecurityAgentQuery", "SecurityAgentQuery(%p) dying", this);
	mConnection->useAgent(NULL);

#if defined(NOSA)
	if (getenv("NOSA")) {
		printf(" [query done]\n");
		return;
	}
#endif		

    if (SecurityAgent::Client::state() != SecurityAgent::Client::dead)
        destroy(); 
}

void
SecurityAgentQuery::activate()
{
	mConnection->useAgent(this);

	try {
		SecurityAgent::Client::activate(mHostInstance->activate());
	} catch (...) {
		mConnection->useAgent(NULL);	// guess not
		throw;
	}
}

void
SecurityAgentQuery::inferHints(Process &thisProcess)
{
	RefPointer<OSXCode> clientCode = thisProcess.clientCode();
	SecurityAgent::RequestorType requestorType = SecurityAgent::unknown;
	string bundlePath;
	
	if (clientCode)
	{
		string encodedBundle = clientCode->encode();
		char bundleType = (encodedBundle.c_str())[0]; // yay, no accessor
		switch(bundleType)
		{
		   case 'b': requestorType = SecurityAgent::bundle; break;
		   case 't': requestorType = SecurityAgent::tool; break;
		}
		bundlePath = clientCode->canonicalPath();
	}

	AuthItemSet processHints = clientHints(requestorType, bundlePath, thisProcess.pid(), thisProcess.uid());
    mClientHints.insert(processHints.begin(), processHints.end());
}

void
SecurityAgentQuery::readChoice()
{
    allow = false;
    remember = false;
    
	AuthItem *allowAction = outContext().find(AGENT_CONTEXT_ALLOW);
	if (allowAction)
	{
	   string allowString;
		if (allowAction->getString(allowString) 
		      && (allowString == "YES"))
		          allow = true;
	}
	
	AuthItem *rememberAction = outContext().find(AGENT_CONTEXT_REMEMBER_ACTION);
	if (rememberAction)
	{
	   string rememberString;
	   if (rememberAction->getString(rememberString)
	           && (rememberString == "YES"))
	               remember = true;
	}
}	

void
SecurityAgentQuery::terminate()
{
	activate();

    // @@@ This happens already in the destructor; presumably we do this to tear things down orderly
	mConnection->useAgent(NULL);
    
	SecurityAgent::Client::terminate();
}

void
SecurityAgentQuery::create(const char *pluginId, const char *mechanismId, const SessionId inSessionId)
{
	activate();
	OSStatus status = SecurityAgent::Client::create(pluginId, mechanismId, inSessionId);
	if (status)
	{
		secdebug("SecurityAgentQuery", "agent went walkabout, restarting");
		Session &session = mHostInstance->session();
		mHostInstance = session.authhost(mAuthHostType, true);
		activate();
		status = SecurityAgent::Client::create(pluginId, mechanismId, inSessionId);
	}
	if (status) MacOSError::throwMe(status);
}

//
// Perform the "rogue app" access query dialog
//
QueryKeychainUse::QueryKeychainUse(bool needPass, const Database *db)
	: mPassphraseCheck(NULL)
{
	// if passphrase checking requested, save KeychainDatabase reference
	// (will quietly disable check if db isn't a keychain)
	if (needPass)
		mPassphraseCheck = dynamic_cast<const KeychainDatabase *>(db);
}

Reason QueryKeychainUse::queryUser (const char *database, const char *description, AclAuthorization action)
{
    Reason reason = SecurityAgent::noReason;
    int retryCount = 0;
	OSStatus status;
	AuthValueVector arguments;
	AuthItemSet hints, context;

#if defined(NOSA)
	if (getenv("NOSA")) {
		char answer[maxPassphraseLength+10];
		
        string applicationPath;
        AuthItem *applicationPathItem = mClientHints.find(AGENT_HINT_APPLICATION_PATH);
		if (applicationPathItem)
		  applicationPathItem->getString(applicationPath);

		getNoSA(answer, sizeof(answer), "Allow %s to do %d on %s in %s? [yn][g]%s ",
			applicationPath.c_str(), int(action), (description ? description : "[NULL item]"),
			(database ? database : "[NULL database]"),
			mPassphraseCheck ? ":passphrase" : "");
		// turn passphrase (no ':') into y:passphrase
		if (mPassphraseCheck && !strchr(answer, ':')) {
			memmove(answer+2, answer, strlen(answer)+1);
			memcpy(answer, "y:", 2);
		}

		allow = answer[0] == 'y';
		remember = answer[1] == 'g';
		return SecurityAgent::noReason;
	}
#endif

	// prepopulate with client hints
	hints.insert(mClientHints.begin(), mClientHints.end());
	
	// put action/operation (sint32) into hints
	hints.insert(AuthItemRef(AGENT_HINT_ACL_TAG, AuthValueOverlay(sizeof(action), static_cast<SInt32*>(&action))));
	
	// item name into hints

	hints.insert(AuthItemRef(AGENT_HINT_KEYCHAIN_ITEM_NAME, AuthValueOverlay(description ? strlen(description) : 0, const_cast<char*>(description))));
	
	// keychain name into hints
	hints.insert(AuthItemRef(AGENT_HINT_KEYCHAIN_PATH, AuthValueOverlay(database ? strlen(database) : 0, const_cast<char*>(database))));
	
	
	if (mPassphraseCheck)
	{
		create("builtin", "confirm-access-password", noSecuritySession);
		
		CssmAutoData data(Allocator::standard(Allocator::sensitive));

		do 
		{

            AuthItemRef triesHint(AGENT_HINT_TRIES, AuthValueOverlay(sizeof(retryCount), &retryCount));
            hints.erase(triesHint); hints.insert(triesHint); // replace
            
            if (retryCount++ > kMaximumAuthorizationTries)
			{
                reason = SecurityAgent::tooManyTries;
			}
				
            AuthItemRef retryHint(AGENT_HINT_RETRY_REASON, AuthValueOverlay(sizeof(reason), &reason));
            hints.erase(retryHint); hints.insert(retryHint); // replace

            setInput(hints, context);
			status = invoke();

            if (retryCount > kMaximumAuthorizationTries)
			{
                return reason;
			}
			
			checkResult();
			
			AuthItem *passwordItem = outContext().find(kAuthorizationEnvironmentPassword);
			if (!passwordItem)
				continue;
						
			passwordItem->getCssmData(data);
		} 
		while (reason = (const_cast<KeychainDatabase*>(mPassphraseCheck)->decode(data) ? SecurityAgent::noReason : SecurityAgent::invalidPassphrase));
	}
	else
	{
		create("builtin", "confirm-access", noSecuritySession);
        setInput(hints, context);
		invoke();
	}
	
    readChoice();
    
	return reason;
}

//
// Perform code signature ACL access adjustment dialogs
//
bool QueryCodeCheck::operator () (const char *aclPath)
{
	OSStatus status;
	AuthValueVector arguments;
	AuthItemSet hints, context;
	
#if defined(NOSA)
	if (getenv("NOSA")) {
		char answer[10];
		
        string applicationPath;
        AuthItem *applicationPathItem = mClientHints.find(AGENT_HINT_APPLICATION_PATH);
		if (applicationPathItem)
		  applicationPathItem->getString(applicationPath);

		getNoSA(answer, sizeof(answer),
				"Allow %s to match an ACL for %s [yn][g]? ",
				applicationPath.c_str(), aclPath ? aclPath : "(unknown)");
		allow = answer[0] == 'y';
		remember = answer[1] == 'g';
		return;
	}
#endif

	// prepopulate with client hints
	hints.insert(mClientHints.begin(), mClientHints.end());
	
	hints.insert(AuthItemRef(AGENT_HINT_APPLICATION_PATH, AuthValueOverlay(strlen(aclPath), const_cast<char*>(aclPath))));
	
	create("builtin", "code-identity", noSecuritySession);

    setInput(hints, context);
	status = invoke();
		
    checkResult();

//	MacOSError::check(status);

    return kAuthorizationResultAllow == result();
}


//
// Obtain passphrases and submit them to the accept() method until it is accepted
// or we can't get another passphrase. Accept() should consume the passphrase
// if it is accepted. If no passphrase is acceptable, throw out of here.
//
Reason QueryOld::query()
{
	Reason reason = SecurityAgent::noReason;
	OSStatus status;
	AuthValueVector arguments;
	AuthItemSet hints, context;
	CssmAutoData passphrase(Allocator::standard(Allocator::sensitive));
	int retryCount = 0;
	
#if defined(NOSA)
    // return the passphrase
	if (getenv("NOSA")) {
        char passphrase_[maxPassphraseLength];
		getNoSA(passphrase, maxPassphraseLength, "Unlock %s [<CR> to cancel]: ", database.dbName());
	passphrase.copy(passphrase_, strlen(passphrase_));
    	return database.decode(passphrase) ? SecurityAgent::noReason : SecurityAgent::invalidPassphrase;
	}
#endif
	
	// prepopulate with client hints

    const char *keychainPath = database.dbName();
    hints.insert(AuthItemRef(AGENT_HINT_KEYCHAIN_PATH, AuthValueOverlay(strlen(keychainPath), const_cast<char*>(keychainPath))));

	hints.insert(mClientHints.begin(), mClientHints.end());

	create("builtin", "unlock-keychain", noSecuritySession);

	do
	{
        AuthItemRef triesHint(AGENT_HINT_TRIES, AuthValueOverlay(sizeof(retryCount), &retryCount));
        hints.erase(triesHint); hints.insert(triesHint); // replace
		
        ++retryCount;
            
        if (retryCount > maxTries)
		{
			reason = SecurityAgent::tooManyTries;
        }
            
        AuthItemRef retryHint(AGENT_HINT_RETRY_REASON, AuthValueOverlay(sizeof(reason), &reason));
        hints.erase(retryHint); hints.insert(retryHint); // replace

        setInput(hints, context);
        status = invoke();

        if (retryCount > maxTries)
        {
			return reason;
		}

        checkResult();
		
		AuthItem *passwordItem = outContext().find(kAuthorizationEnvironmentPassword);
		if (!passwordItem)
			continue;
		
		passwordItem->getCssmData(passphrase);
		
	}
	while (reason = accept(passphrase));

	return SecurityAgent::noReason;
}


//
// Get existing passphrase (unlock) Query
//
Reason QueryOld::operator () ()
{
	return query();
}


//
// End-classes for old secrets
//
Reason QueryUnlock::accept(CssmManagedData &passphrase)
{
	if (safer_cast<KeychainDatabase &>(database).decode(passphrase))
		return SecurityAgent::noReason;
	else
		return SecurityAgent::invalidPassphrase;
}


QueryPIN::QueryPIN(Database &db)
	: QueryOld(db), mPin(Allocator::standard())
{
	this->inferHints(Server::process());
}


Reason QueryPIN::accept(CssmManagedData &pin)
{
	// no retries for now
	mPin = pin;
	return SecurityAgent::noReason;
}


//
// Obtain passphrases and submit them to the accept() method until it is accepted
// or we can't get another passphrase. Accept() should consume the passphrase
// if it is accepted. If no passphrase is acceptable, throw out of here.
//
Reason QueryNewPassphrase::query()
{
	Reason reason = initialReason;
	CssmAutoData passphrase(Allocator::standard(Allocator::sensitive));
	CssmAutoData oldPassphrase(Allocator::standard(Allocator::sensitive));

    OSStatus status;
	AuthValueVector arguments;
	AuthItemSet hints, context;
    
	int retryCount = 0;

#if defined(NOSA)
	if (getenv("NOSA")) {
        char passphrase_[maxPassphraseLength];
		getNoSA(passphrase_, maxPassphraseLength,
			"New passphrase for %s (reason %d) [<CR> to cancel]: ",
			database.dbName(), reason);
		return SecurityAgent::noReason;
	}
#endif

	// prepopulate with client hints
	hints.insert(mClientHints.begin(), mClientHints.end());

	// keychain name into hints
	hints.insert(AuthItemRef(AGENT_HINT_KEYCHAIN_PATH, AuthValueOverlay(database.dbName())));

    switch (initialReason)
    {
        case SecurityAgent::newDatabase: 
            create("builtin", "new-passphrase", noSecuritySession);
            break;
        case SecurityAgent::changePassphrase:
            create("builtin", "change-passphrase", noSecuritySession);
            break;
        default:
            assert(false);
    }

	do
	{
        AuthItemRef triesHint(AGENT_HINT_TRIES, AuthValueOverlay(sizeof(retryCount), &retryCount));
        hints.erase(triesHint); hints.insert(triesHint); // replace

		if (++retryCount > maxTries)
		{
			reason = SecurityAgent::tooManyTries;
		}

        AuthItemRef retryHint(AGENT_HINT_RETRY_REASON, AuthValueOverlay(sizeof(reason), &reason));
        hints.erase(retryHint); hints.insert(retryHint); // replace

        setInput(hints, context);
		status = invoke();

		if (retryCount > maxTries)
		{
            return reason;
        }
        
        checkResult();

		if (SecurityAgent::changePassphrase == initialReason)
        {
            AuthItem *oldPasswordItem = outContext().find(AGENT_PASSWORD);
            if (!oldPasswordItem)
                continue;
            
            oldPasswordItem->getCssmData(oldPassphrase);
        }
        
		AuthItem *passwordItem = outContext().find(AGENT_CONTEXT_NEW_PASSWORD);
		if (!passwordItem)
			continue;
		
		passwordItem->getCssmData(passphrase);

    }
	while (reason = accept(passphrase, (initialReason == SecurityAgent::changePassphrase) ? &oldPassphrase.get() : NULL));
    
	return SecurityAgent::noReason;
}


//
// Get new passphrase Query
//
Reason QueryNewPassphrase::operator () (CssmOwnedData &passphrase)
{
	if (Reason result = query())
		return result;	// failed
	passphrase = mPassphrase;
	return SecurityAgent::noReason;	// success
}

Reason QueryNewPassphrase::accept(CssmManagedData &passphrase, CssmData *oldPassphrase)
{
	//@@@ acceptance criteria are currently hardwired here
	//@@@ This validation presumes ASCII - UTF8 might be more lenient
	
	// if we have an old passphrase, check it
	if (oldPassphrase && !safer_cast<KeychainDatabase&>(database).validatePassphrase(*oldPassphrase))
		return SecurityAgent::oldPassphraseWrong;
	
	// sanity check the new passphrase (but allow user override)
	if (!(mPassphraseValid && passphrase.get() == mPassphrase)) {
		mPassphrase = passphrase;
		mPassphraseValid = true;
		if (mPassphrase.length() == 0)
			return SecurityAgent::passphraseIsNull;
		if (mPassphrase.length() < 6)
			return SecurityAgent::passphraseTooSimple;
	}
	
	// accept this
	return SecurityAgent::noReason;
}

// 
// Get a passphrase for unspecified use
// 
Reason QueryGenericPassphrase::operator () (const char *prompt, bool verify, 
                                            string &passphrase)
{
    return query(prompt, verify, passphrase);
}

Reason QueryGenericPassphrase::query(const char *prompt, bool verify, 
                                     string &passphrase)
{
    Reason reason = SecurityAgent::noReason;
    OSStatus status;    // not really used; remove?  
    AuthValueVector arguments;
    AuthItemSet hints, context;
	
#if defined(NOSA)
    if (getenv("NOSA")) {
		// FIXME  3690984
		return SecurityAgent::noReason;
    }
#endif
	
    hints.insert(mClientHints.begin(), mClientHints.end());
    hints.insert(AuthItemRef(AGENT_HINT_CUSTOM_PROMPT, AuthValueOverlay(prompt ? strlen(prompt) : 0, const_cast<char*>(prompt))));
    // XXX/gh  defined by dmitch but no analogous hint in
    // AuthorizationTagsPriv.h:
    // CSSM_ATTRIBUTE_ALERT_TITLE (optional alert panel title)
	
    if (false == verify) {  // import
		create("builtin", "generic-unlock", noSecuritySession);
    } else {		// verify passphrase (export)
					// new-passphrase-generic works with the pre-4 June 2004 agent; 
					// generic-new-passphrase is required for the new agent
		create("builtin", "generic-new-passphrase", noSecuritySession);
    }
    
    AuthItem *passwordItem;
    
    do {
        setInput(hints, context);
		status = invoke();
		checkResult();
		passwordItem = outContext().find(AGENT_PASSWORD);
		
    } while (!passwordItem);
	
    passwordItem->getString(passphrase);
    
    return reason;
}


// 
// Get a DB blob's passphrase--keychain synchronization
// 

void QueryDBBlobSecret::addHint(const char *name, const void *value, UInt32 valueLen, UInt32 flags)
{
    AuthorizationItem item = { name, valueLen, const_cast<void *>(value), flags };
    mClientHints.insert(AuthItemRef(item));
}

Reason QueryDBBlobSecret::operator () (DatabaseCryptoCore &dbCore, const DbBlob *secretsBlob)
{
    return query(dbCore, secretsBlob);
}

Reason QueryDBBlobSecret::query(DatabaseCryptoCore &dbCore, const DbBlob *secretsBlob)
{
    Reason reason = SecurityAgent::noReason;
	CssmAutoData passphrase(Allocator::standard(Allocator::sensitive));
    OSStatus status;    // not really used; remove?  
    AuthValueVector arguments;
    AuthItemSet hints/*NUKEME*/, context;
	
#if defined(NOSA)
    if (getenv("NOSA")) {
		// FIXME  akin to 3690984
		return SecurityAgent::noReason;
    }
#endif
	
    hints.insert(mClientHints.begin(), mClientHints.end());
	
	create("builtin", "generic-unlock-kcblob", noSecuritySession);
    
    AuthItem *secretItem;
    
	int retryCount = 0;
	
    do {
        AuthItemRef triesHint(AGENT_HINT_TRIES, AuthValueOverlay(sizeof(retryCount), &retryCount));
        hints.erase(triesHint); hints.insert(triesHint); // replace

		if (++retryCount > maxTries)
		{
			reason = SecurityAgent::tooManyTries;
		}		
		
        AuthItemRef retryHint(AGENT_HINT_RETRY_REASON, AuthValueOverlay(sizeof(reason), &reason));
        hints.erase(retryHint); hints.insert(retryHint); // replace
		
        setInput(hints, context);
		status = invoke();
		checkResult();
		secretItem = outContext().find(AGENT_PASSWORD);
		if (!secretItem)
			continue;
		secretItem->getCssmData(passphrase);
		
    } while (reason = accept(passphrase, dbCore, secretsBlob));
	    
    return reason;
}

Reason QueryDBBlobSecret::accept(CssmManagedData &passphrase, 
								 DatabaseCryptoCore &dbCore, 
								 const DbBlob *secretsBlob)
{
	try {
		dbCore.setup(secretsBlob, passphrase);
		dbCore.decodeCore(secretsBlob, NULL);
	} catch (const CommonError &err) {
		// XXX/gh  Are there errors other than this?  
		return SecurityAgent::invalidPassphrase;
	}
	return SecurityAgent::noReason;
}

QueryInvokeMechanism::QueryInvokeMechanism(const AuthHostType type, Session &session) :
    SecurityAgentQuery(type, session) { }

void QueryInvokeMechanism::initialize(const string &inPluginId, const string &inMechanismId, const AuthValueVector &inArguments, const SessionId inSessionId)
{
    if (SecurityAgent::Client::init == SecurityAgent::Client::state())
    {
        create(inPluginId.c_str(), inMechanismId.c_str(), inSessionId);
        mArguments = inArguments;
    }
}

// XXX/cs should return AuthorizationResult
void QueryInvokeMechanism::run(const AuthValueVector &inArguments, AuthItemSet &inHints, AuthItemSet &inContext, AuthorizationResult *outResult)
{
    // prepopulate with client hints
	inHints.insert(mClientHints.begin(), mClientHints.end());

    setArguments(inArguments);
    setInput(inHints, inContext);
    MacOSError::check(invoke());
    
	if (outResult) *outResult = result();
    
    inHints = outHints();
    inContext = outContext();
}

void QueryInvokeMechanism::terminateAgent()
{
    terminate();
}