2 * Copyright (c) 2000-2004 Apple Computer, Inc. All Rights Reserved.
4 * @APPLE_LICENSE_HEADER_START@
6 * This file contains Original Code and/or Modifications of Original Code
7 * as defined in and that are subject to the Apple Public Source License
8 * Version 2.0 (the 'License'). You may not use this file except in
9 * compliance with the License. Please obtain a copy of the License at
10 * http://www.opensource.apple.com/apsl/ and read it before using this
13 * The Original Code and all software distributed under the License are
14 * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
15 * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
16 * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
17 * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
18 * Please see the License for the specific language governing rights and
19 * limitations under the License.
21 * @APPLE_LICENSE_HEADER_END@
25 // passphrases - canonical code to obtain passphrases
27 #include "agentquery.h"
28 #include "authority.h"
30 #include <Security/AuthorizationTags.h>
31 #include <Security/AuthorizationTagsPriv.h>
34 // NOSA support functions. This is a test mode where the SecurityAgent
35 // is simulated via stdio in the client. Good for running automated tests
36 // of client programs. Only available if -DNOSA when compiling.
42 static void getNoSA(char *buffer
, size_t bufferSize
, const char *fmt
, ...)
47 vfprintf(stdout
, fmt
, args
);
51 memset(buffer
, 0, bufferSize
);
52 const char *nosa
= getenv("NOSA");
53 if (!strcmp(nosa
, "-")) {
54 if (fgets(buffer
, bufferSize
-1, stdin
) == NULL
)
55 CssmError::throwMe(CSSM_ERRCODE_NO_USER_INTERACTION
);
56 buffer
[strlen(buffer
)-1] = '\0'; // remove trailing newline
57 if (!isatty(fileno(stdin
)))
58 printf("%s\n", buffer
); // echo to output if input not terminal
60 strncpy(buffer
, nosa
, bufferSize
-1);
61 printf("%s\n", buffer
);
63 if (buffer
[0] == '\0') // empty input -> cancellation
64 CssmError::throwMe(CSSM_ERRCODE_USER_CANCELED
);
70 using SecurityAgent::Reason
;
71 using namespace Authorization
;
73 SecurityAgentQuery::SecurityAgentQuery(const AuthHostType type
, Session
&session
) : mAuthHostType(type
), mHostInstance(session
.authhost(mAuthHostType
)), mConnection(&Server::connection())
75 secdebug("SecurityAgentQuery", "new SecurityAgentQuery(%p)", this);
78 SecurityAgentQuery::~SecurityAgentQuery()
80 secdebug("SecurityAgentQuery", "SecurityAgentQuery(%p) dying", this);
81 mConnection
->useAgent(NULL
);
85 printf(" [query done]\n");
90 if (SecurityAgent::Client::state() != SecurityAgent::Client::dead
)
95 SecurityAgentQuery::activate()
97 // this may take a while
98 Server::active().longTermActivity();
99 mConnection
->useAgent(this);
102 SecurityAgent::Client::activate(mHostInstance
->activate());
104 mConnection
->useAgent(NULL
); // guess not
110 SecurityAgentQuery::inferHints(Process
&thisProcess
)
112 RefPointer
<OSXCode
> clientCode
= thisProcess
.clientCode();
113 SecurityAgent::RequestorType requestorType
= SecurityAgent::unknown
;
118 string encodedBundle
= clientCode
->encode();
119 char bundleType
= (encodedBundle
.c_str())[0]; // yay, no accessor
122 case 'b': requestorType
= SecurityAgent::bundle
; break;
123 case 't': requestorType
= SecurityAgent::tool
; break;
125 bundlePath
= clientCode
->canonicalPath();
128 AuthItemSet processHints
= clientHints(requestorType
, bundlePath
, thisProcess
.pid(), thisProcess
.uid());
129 mClientHints
.insert(processHints
.begin(), processHints
.end());
133 SecurityAgentQuery::readChoice()
138 AuthItem
*allowAction
= outContext().find(AGENT_CONTEXT_ALLOW
);
142 if (allowAction
->getString(allowString
)
143 && (allowString
== "YES"))
147 AuthItem
*rememberAction
= outContext().find(AGENT_CONTEXT_REMEMBER_ACTION
);
150 string rememberString
;
151 if (rememberAction
->getString(rememberString
)
152 && (rememberString
== "YES"))
158 SecurityAgentQuery::terminate()
162 // @@@ This happens already in the destructor; presumably we do this to tear things down orderly
163 mConnection
->useAgent(NULL
);
165 SecurityAgent::Client::terminate();
169 SecurityAgentQuery::create(const char *pluginId
, const char *mechanismId
, const SessionId inSessionId
)
172 OSStatus status
= SecurityAgent::Client::create(pluginId
, mechanismId
, inSessionId
);
175 secdebug("SecurityAgentQuery", "agent went walkabout, restarting");
176 Session
&session
= mHostInstance
->session();
177 mHostInstance
= session
.authhost(mAuthHostType
, true);
179 status
= SecurityAgent::Client::create(pluginId
, mechanismId
, inSessionId
);
181 if (status
) MacOSError::throwMe(status
);
185 // Perform the "rogue app" access query dialog
187 QueryKeychainUse::QueryKeychainUse(bool needPass
, const Database
*db
)
188 : mPassphraseCheck(NULL
)
190 // if passphrase checking requested, save KeychainDatabase reference
191 // (will quietly disable check if db isn't a keychain)
193 mPassphraseCheck
= dynamic_cast<const KeychainDatabase
*>(db
);
196 Reason
QueryKeychainUse::queryUser (const char *database
, const char *description
, AclAuthorization action
)
198 Reason reason
= SecurityAgent::noReason
;
201 AuthValueVector arguments
;
202 AuthItemSet hints
, context
;
205 if (getenv("NOSA")) {
206 char answer
[maxPassphraseLength
+10];
208 string applicationPath
;
209 AuthItem
*applicationPathItem
= mClientHints
.find(AGENT_HINT_APPLICATION_PATH
);
210 if (applicationPathItem
)
211 applicationPathItem
->getString(applicationPath
);
213 getNoSA(answer
, sizeof(answer
), "Allow %s to do %d on %s in %s? [yn][g]%s ",
214 applicationPath
.c_str(), int(action
), (description
? description
: "[NULL item]"),
215 (database
? database
: "[NULL database]"),
216 mPassphraseCheck
? ":passphrase" : "");
217 // turn passphrase (no ':') into y:passphrase
218 if (mPassphraseCheck
&& !strchr(answer
, ':')) {
219 memmove(answer
+2, answer
, strlen(answer
)+1);
220 memcpy(answer
, "y:", 2);
223 allow
= answer
[0] == 'y';
224 remember
= answer
[1] == 'g';
225 return SecurityAgent::noReason
;
229 // prepopulate with client hints
230 hints
.insert(mClientHints
.begin(), mClientHints
.end());
232 // put action/operation (sint32) into hints
233 hints
.insert(AuthItemRef(AGENT_HINT_ACL_TAG
, AuthValueOverlay(sizeof(action
), static_cast<SInt32
*>(&action
))));
235 // item name into hints
237 hints
.insert(AuthItemRef(AGENT_HINT_KEYCHAIN_ITEM_NAME
, AuthValueOverlay(description
? strlen(description
) : 0, const_cast<char*>(description
))));
239 // keychain name into hints
240 hints
.insert(AuthItemRef(AGENT_HINT_KEYCHAIN_PATH
, AuthValueOverlay(database
? strlen(database
) : 0, const_cast<char*>(database
))));
243 if (mPassphraseCheck
)
245 create("builtin", "confirm-access-password", NULL
);
247 CssmAutoData
data(Allocator::standard(Allocator::sensitive
));
252 AuthItemRef
triesHint(AGENT_HINT_TRIES
, AuthValueOverlay(sizeof(retryCount
), &retryCount
));
253 hints
.erase(triesHint
); hints
.insert(triesHint
); // replace
255 if (retryCount
++ > kMaximumAuthorizationTries
)
257 reason
= SecurityAgent::tooManyTries
;
260 AuthItemRef
retryHint(AGENT_HINT_RETRY_REASON
, AuthValueOverlay(sizeof(reason
), &reason
));
261 hints
.erase(retryHint
); hints
.insert(retryHint
); // replace
263 setInput(hints
, context
);
266 if (retryCount
> kMaximumAuthorizationTries
)
273 AuthItem
*passwordItem
= outContext().find(kAuthorizationEnvironmentPassword
);
277 passwordItem
->getCssmData(data
);
279 while (reason
= (const_cast<KeychainDatabase
*>(mPassphraseCheck
)->decode(data
) ? SecurityAgent::noReason
: SecurityAgent::invalidPassphrase
));
283 create("builtin", "confirm-access", NULL
);
284 setInput(hints
, context
);
294 // Perform code signature ACL access adjustment dialogs
296 bool QueryCodeCheck::operator () (const char *aclPath
)
299 AuthValueVector arguments
;
300 AuthItemSet hints
, context
;
303 if (getenv("NOSA")) {
306 string applicationPath
;
307 AuthItem
*applicationPathItem
= mClientHints
.find(AGENT_HINT_APPLICATION_PATH
);
308 if (applicationPathItem
)
309 applicationPathItem
->getString(applicationPath
);
311 getNoSA(answer
, sizeof(answer
),
312 "Allow %s to match an ACL for %s [yn][g]? ",
313 applicationPath
.c_str(), aclPath
? aclPath
: "(unknown)");
314 allow
= answer
[0] == 'y';
315 remember
= answer
[1] == 'g';
320 // prepopulate with client hints
321 hints
.insert(mClientHints
.begin(), mClientHints
.end());
323 hints
.insert(AuthItemRef(AGENT_HINT_APPLICATION_PATH
, AuthValueOverlay(strlen(aclPath
), const_cast<char*>(aclPath
))));
325 create("builtin", "code-identity", NULL
);
327 setInput(hints
, context
);
332 // MacOSError::check(status);
334 return kAuthorizationResultAllow
== result();
339 // Obtain passphrases and submit them to the accept() method until it is accepted
340 // or we can't get another passphrase. Accept() should consume the passphrase
341 // if it is accepted. If no passphrase is acceptable, throw out of here.
343 Reason
QueryOld::query()
345 Reason reason
= SecurityAgent::noReason
;
347 AuthValueVector arguments
;
348 AuthItemSet hints
, context
;
349 CssmAutoData
passphrase(Allocator::standard(Allocator::sensitive
));
353 // return the passphrase
354 if (getenv("NOSA")) {
355 char passphrase_
[maxPassphraseLength
];
356 getNoSA(passphrase
, maxPassphraseLength
, "Unlock %s [<CR> to cancel]: ", database
.dbName());
357 passphrase
.copy(passphrase_
, strlen(passphrase_
));
358 return database
.decode(passphrase
) ? SecurityAgent::noReason
: SecurityAgent::invalidPassphrase
;
362 // prepopulate with client hints
364 const char *keychainPath
= database
.dbName();
365 hints
.insert(AuthItemRef(AGENT_HINT_KEYCHAIN_PATH
, AuthValueOverlay(strlen(keychainPath
), const_cast<char*>(keychainPath
))));
367 hints
.insert(mClientHints
.begin(), mClientHints
.end());
369 create("builtin", "unlock-keychain", NULL
);
373 AuthItemRef
triesHint(AGENT_HINT_TRIES
, AuthValueOverlay(sizeof(retryCount
), &retryCount
));
374 hints
.erase(triesHint
); hints
.insert(triesHint
); // replace
378 if (retryCount
> maxTries
)
380 reason
= SecurityAgent::tooManyTries
;
383 AuthItemRef
retryHint(AGENT_HINT_RETRY_REASON
, AuthValueOverlay(sizeof(reason
), &reason
));
384 hints
.erase(retryHint
); hints
.insert(retryHint
); // replace
386 setInput(hints
, context
);
389 if (retryCount
> maxTries
)
396 AuthItem
*passwordItem
= outContext().find(kAuthorizationEnvironmentPassword
);
400 passwordItem
->getCssmData(passphrase
);
403 while (reason
= accept(passphrase
));
405 return SecurityAgent::noReason
;
410 // Get existing passphrase (unlock) Query
412 Reason
QueryOld::operator () ()
419 // End-classes for old secrets
421 Reason
QueryUnlock::accept(CssmManagedData
&passphrase
)
423 if (safer_cast
<KeychainDatabase
&>(database
).decode(passphrase
))
424 return SecurityAgent::noReason
;
426 return SecurityAgent::invalidPassphrase
;
430 QueryPIN::QueryPIN(Database
&db
)
431 : QueryOld(db
), mPin(Allocator::standard())
433 this->inferHints(Server::process());
437 Reason
QueryPIN::accept(CssmManagedData
&pin
)
439 // no retries for now
441 return SecurityAgent::noReason
;
446 // Obtain passphrases and submit them to the accept() method until it is accepted
447 // or we can't get another passphrase. Accept() should consume the passphrase
448 // if it is accepted. If no passphrase is acceptable, throw out of here.
450 Reason
QueryNewPassphrase::query()
452 Reason reason
= initialReason
;
453 CssmAutoData
passphrase(Allocator::standard(Allocator::sensitive
));
454 CssmAutoData
oldPassphrase(Allocator::standard(Allocator::sensitive
));
457 AuthValueVector arguments
;
458 AuthItemSet hints
, context
;
463 if (getenv("NOSA")) {
464 char passphrase_
[maxPassphraseLength
];
465 getNoSA(passphrase_
, maxPassphraseLength
,
466 "New passphrase for %s (reason %d) [<CR> to cancel]: ",
467 database
.dbName(), reason
);
468 return SecurityAgent::noReason
;
472 // prepopulate with client hints
473 hints
.insert(mClientHints
.begin(), mClientHints
.end());
475 // keychain name into hints
476 hints
.insert(AuthItemRef(AGENT_HINT_KEYCHAIN_PATH
, AuthValueOverlay(database
.dbName())));
478 switch (initialReason
)
480 case SecurityAgent::newDatabase
:
481 create("builtin", "new-passphrase", NULL
);
483 case SecurityAgent::changePassphrase
:
484 create("builtin", "change-passphrase", NULL
);
492 AuthItemRef
triesHint(AGENT_HINT_TRIES
, AuthValueOverlay(sizeof(retryCount
), &retryCount
));
493 hints
.erase(triesHint
); hints
.insert(triesHint
); // replace
495 if (++retryCount
> maxTries
)
497 reason
= SecurityAgent::tooManyTries
;
500 AuthItemRef
retryHint(AGENT_HINT_RETRY_REASON
, AuthValueOverlay(sizeof(reason
), &reason
));
501 hints
.erase(retryHint
); hints
.insert(retryHint
); // replace
503 setInput(hints
, context
);
506 if (retryCount
> maxTries
)
513 if (SecurityAgent::changePassphrase
== initialReason
)
515 AuthItem
*oldPasswordItem
= outContext().find(AGENT_PASSWORD
);
516 if (!oldPasswordItem
)
519 oldPasswordItem
->getCssmData(oldPassphrase
);
522 AuthItem
*passwordItem
= outContext().find(AGENT_CONTEXT_NEW_PASSWORD
);
526 passwordItem
->getCssmData(passphrase
);
529 while (reason
= accept(passphrase
, (initialReason
== SecurityAgent::changePassphrase
) ? &oldPassphrase
.get() : NULL
));
531 return SecurityAgent::noReason
;
536 // Get new passphrase Query
538 Reason
QueryNewPassphrase::operator () (CssmOwnedData
&passphrase
)
540 if (Reason result
= query())
541 return result
; // failed
542 passphrase
= mPassphrase
;
543 return SecurityAgent::noReason
; // success
546 Reason
QueryNewPassphrase::accept(CssmManagedData
&passphrase
, CssmData
*oldPassphrase
)
548 //@@@ acceptance criteria are currently hardwired here
549 //@@@ This validation presumes ASCII - UTF8 might be more lenient
551 // if we have an old passphrase, check it
552 if (oldPassphrase
&& !safer_cast
<KeychainDatabase
&>(database
).validatePassphrase(*oldPassphrase
))
553 return SecurityAgent::oldPassphraseWrong
;
555 // sanity check the new passphrase (but allow user override)
556 if (!(mPassphraseValid
&& passphrase
.get() == mPassphrase
)) {
557 mPassphrase
= passphrase
;
558 mPassphraseValid
= true;
559 if (mPassphrase
.length() == 0)
560 return SecurityAgent::passphraseIsNull
;
561 if (mPassphrase
.length() < 6)
562 return SecurityAgent::passphraseTooSimple
;
566 return SecurityAgent::noReason
;
570 // Get a passphrase for unspecified use
572 Reason
QueryGenericPassphrase::operator () (const char *prompt
, bool verify
,
575 return query(prompt
, verify
, passphrase
);
578 Reason
QueryGenericPassphrase::query(const char *prompt
, bool verify
,
581 Reason reason
= SecurityAgent::noReason
;
582 OSStatus status
; // not really used; remove?
583 AuthValueVector arguments
;
584 AuthItemSet hints
, context
;
587 if (getenv("NOSA")) {
589 return SecurityAgent::noReason
;
593 hints
.insert(mClientHints
.begin(), mClientHints
.end());
594 hints
.insert(AuthItemRef(AGENT_HINT_CUSTOM_PROMPT
, AuthValueOverlay(prompt
? strlen(prompt
) : 0, const_cast<char*>(prompt
))));
595 // XXX/gh defined by dmitch but no analogous hint in
596 // AuthorizationTagsPriv.h:
597 // CSSM_ATTRIBUTE_ALERT_TITLE (optional alert panel title)
599 if (false == verify
) { // import
600 create("builtin", "generic-unlock", NULL
);
601 } else { // verify passphrase (export)
602 // new-passphrase-generic works with the pre-4 June 2004 agent;
603 // generic-new-passphrase is required for the new agent
604 create("builtin", "generic-new-passphrase", NULL
);
607 AuthItem
*passwordItem
;
610 setInput(hints
, context
);
613 passwordItem
= outContext().find(AGENT_PASSWORD
);
615 } while (!passwordItem
);
617 passwordItem
->getString(passphrase
);
624 // Get a DB blob's passphrase--keychain synchronization
627 void QueryDBBlobSecret::addHint(const char *name
, const void *value
, UInt32 valueLen
, UInt32 flags
)
629 AuthorizationItem item
= { name
, valueLen
, const_cast<void *>(value
), flags
};
630 mClientHints
.insert(AuthItemRef(item
));
633 Reason
QueryDBBlobSecret::operator () (DatabaseCryptoCore
&dbCore
, const DbBlob
*secretsBlob
)
635 return query(dbCore
, secretsBlob
);
638 Reason
QueryDBBlobSecret::query(DatabaseCryptoCore
&dbCore
, const DbBlob
*secretsBlob
)
640 Reason reason
= SecurityAgent::noReason
;
641 CssmAutoData
passphrase(Allocator::standard(Allocator::sensitive
));
642 OSStatus status
; // not really used; remove?
643 AuthValueVector arguments
;
644 AuthItemSet hints
/*NUKEME*/, context
;
647 if (getenv("NOSA")) {
648 // FIXME akin to 3690984
649 return SecurityAgent::noReason
;
653 hints
.insert(mClientHints
.begin(), mClientHints
.end());
655 create("builtin", "generic-unlock-kcblob", NULL
);
657 AuthItem
*secretItem
;
662 AuthItemRef
triesHint(AGENT_HINT_TRIES
, AuthValueOverlay(sizeof(retryCount
), &retryCount
));
663 hints
.erase(triesHint
); hints
.insert(triesHint
); // replace
665 if (++retryCount
> maxTries
)
667 reason
= SecurityAgent::tooManyTries
;
670 AuthItemRef
retryHint(AGENT_HINT_RETRY_REASON
, AuthValueOverlay(sizeof(reason
), &reason
));
671 hints
.erase(retryHint
); hints
.insert(retryHint
); // replace
673 setInput(hints
, context
);
676 secretItem
= outContext().find(AGENT_PASSWORD
);
679 secretItem
->getCssmData(passphrase
);
681 } while (reason
= accept(passphrase
, dbCore
, secretsBlob
));
686 Reason
QueryDBBlobSecret::accept(CssmManagedData
&passphrase
,
687 DatabaseCryptoCore
&dbCore
,
688 const DbBlob
*secretsBlob
)
691 dbCore
.setup(secretsBlob
, passphrase
);
692 dbCore
.decodeCore(secretsBlob
, NULL
);
693 } catch (const CommonError
&err
) {
694 // XXX/gh Are there errors other than this?
695 return SecurityAgent::invalidPassphrase
;
697 return SecurityAgent::noReason
;
700 QueryInvokeMechanism::QueryInvokeMechanism(const AuthHostType type
, Session
&session
) :
701 SecurityAgentQuery(type
, session
) { }
703 void QueryInvokeMechanism::initialize(const string
&inPluginId
, const string
&inMechanismId
, const AuthValueVector
&inArguments
, const SessionId inSessionId
)
705 if (SecurityAgent::Client::init
== SecurityAgent::Client::state())
707 create(inPluginId
.c_str(), inMechanismId
.c_str(), inSessionId
);
708 mArguments
= inArguments
;
712 // XXX/cs should return AuthorizationResult
713 void QueryInvokeMechanism::run(const AuthValueVector
&inArguments
, AuthItemSet
&inHints
, AuthItemSet
&inContext
, AuthorizationResult
*outResult
)
715 // prepopulate with client hints
716 inHints
.insert(mClientHints
.begin(), mClientHints
.end());
718 setArguments(inArguments
);
719 setInput(inHints
, inContext
);
720 MacOSError::check(invoke());
722 if (outResult
) *outResult
= result();
724 inHints
= outHints();
725 inContext
= outContext();
728 void QueryInvokeMechanism::terminateAgent()