2 * Copyright (c) 2000-2004 Apple Computer, Inc. All Rights Reserved.
4 * @APPLE_LICENSE_HEADER_START@
6 * Copyright (c) 1999-2003 Apple Computer, Inc. All Rights Reserved.
8 * This file contains Original Code and/or Modifications of Original Code
9 * as defined in and that are subject to the Apple Public Source License
10 * Version 2.0 (the 'License'). You may not use this file except in
11 * compliance with the License. Please obtain a copy of the License at
12 * http://www.opensource.apple.com/apsl/ and read it before using this
15 * The Original Code and all software distributed under the License are
16 * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
17 * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
18 * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
19 * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
20 * Please see the License for the specific language governing rights and
21 * limitations under the License.
23 * @APPLE_LICENSE_HEADER_END@
27 // passphrases - canonical code to obtain passphrases
29 #include "agentquery.h"
30 #include "authority.h"
34 #include <Security/AuthorizationTags.h>
35 #include <Security/AuthorizationTagsPriv.h>
38 // NOSA support functions. This is a test mode where the SecurityAgent
39 // is simulated via stdio in the client. Good for running automated tests
40 // of client programs. Only available if -DNOSA when compiling.
46 static void getNoSA(char *buffer
, size_t bufferSize
, const char *fmt
, ...)
51 vfprintf(stdout
, fmt
, args
);
55 memset(buffer
, 0, bufferSize
);
56 const char *nosa
= getenv("NOSA");
57 if (!strcmp(nosa
, "-")) {
58 if (fgets(buffer
, bufferSize
-1, stdin
) == NULL
)
59 CssmError::throwMe(CSSM_ERRCODE_NO_USER_INTERACTION
);
60 buffer
[strlen(buffer
)-1] = '\0'; // remove trailing newline
61 if (!isatty(fileno(stdin
)))
62 printf("%s\n", buffer
); // echo to output if input not terminal
64 strncpy(buffer
, nosa
, bufferSize
-1);
65 printf("%s\n", buffer
);
67 if (buffer
[0] == '\0') // empty input -> cancellation
68 CssmError::throwMe(CSSM_ERRCODE_USER_CANCELED
);
75 // The default Mach service name for SecurityAgent
77 const char SecurityAgentQuery::defaultName
[] = "com.apple.SecurityAgent";
79 using SecurityAgent::Reason
;
80 using namespace Authorization
;
83 // Construct a query object
86 SecurityAgentQuery::SecurityAgentQuery() :
87 SecurityAgent::Client(Server::process().uid(),
88 Server::session().bootstrapPort(),
90 mClientSession(Server::session())
92 secdebug("SecurityAgentQuery", "new query");
94 // XXX/cs set up the general settings for the client as hints
95 // any invoke will merge these and whatever the client passes on invoke
99 SecurityAgentQuery::SecurityAgentQuery(uid_t clientUID
,
100 const Session
&clientSession
,
101 const char *agentName
) :
102 SecurityAgent::Client(clientUID
, clientSession
.bootstrapPort(), agentName
? agentName
: defaultName
),
103 mClientSession(clientSession
)
105 secdebug("SecurityAgentQuery", "new query");
108 SecurityAgentQuery::~SecurityAgentQuery()
110 secdebug("SecurityAgentQuery", "query dying");
111 Server::connection().useAgent(NULL
);
114 if (getenv("NOSA")) {
115 printf(" [query done]\n");
125 SecurityAgentQuery::activate()
130 // Before popping up an agent: is UI session allowed?
131 if (!(mClientSession
.attributes() & sessionHasGraphicAccess
))
132 CssmError::throwMe(CSSM_ERRCODE_NO_USER_INTERACTION
);
134 // this may take a while
135 Server::active().longTermActivity();
136 Server::connection().useAgent(this);
139 SecurityAgent::Client::activate();
141 Server::connection().useAgent(NULL
); // guess not
150 SecurityAgentQuery::inferHints(Process
&thisProcess
)
152 AuthItemSet processHints
= clientHints(thisProcess
.clientCode(), thisProcess
.pid(), thisProcess
.uid());
153 mClientHints
.insert(processHints
.begin(), processHints
.end());
157 SecurityAgentQuery::readChoice()
162 AuthItem
*allowAction
= mContext
.find(AGENT_CONTEXT_ALLOW
);
166 if (allowAction
->getString(allowString
)
167 && (allowString
== "YES"))
171 AuthItem
*rememberAction
= mContext
.find(AGENT_CONTEXT_REMEMBER_ACTION
);
174 string rememberString
;
175 if (rememberAction
->getString(rememberString
)
176 && (rememberString
== "YES"))
182 SecurityAgentQuery::terminate()
187 // @@@ This happens already in the destructor; presumably we do this to tear things down orderly
188 Server::connection(true).useAgent(NULL
);
190 SecurityAgent::Client::terminate();
195 // Perform the "rogue app" access query dialog
197 QueryKeychainUse::QueryKeychainUse(bool needPass
, const Database
*db
)
198 : mPassphraseCheck(NULL
)
200 // if passphrase checking requested, save KeychainDatabase reference
201 // (will quietly disable check if db isn't a keychain)
203 mPassphraseCheck
= dynamic_cast<const KeychainDatabase
*>(db
);
206 Reason
QueryKeychainUse::queryUser (const char *database
, const char *description
, AclAuthorization action
)
208 Reason reason
= SecurityAgent::noReason
;
211 AuthValueVector arguments
;
212 AuthItemSet hints
, context
;
215 if (getenv("NOSA")) {
216 char answer
[maxPassphraseLength
+10];
218 string applicationPath
;
219 AuthItem
*applicationPathItem
= mClientHints
.find(AGENT_HINT_APPLICATION_PATH
);
220 if (applicationPathItem
)
221 applicationPathItem
->getString(applicationPath
);
223 getNoSA(answer
, sizeof(answer
), "Allow %s to do %d on %s in %s? [yn][g]%s ",
224 applicationPath
.c_str(), int(action
), (description
? description
: "[NULL item]"),
225 (database
? database
: "[NULL database]"),
226 mPassphraseCheck
? ":passphrase" : "");
227 // turn passphrase (no ':') into y:passphrase
228 if (mPassphraseCheck
&& !strchr(answer
, ':')) {
229 memmove(answer
+2, answer
, strlen(answer
)+1);
230 memcpy(answer
, "y:", 2);
233 allow
= answer
[0] == 'y';
234 remember
= answer
[1] == 'g';
235 return SecurityAgent::noReason
;
241 // prepopulate with client hints
242 hints
.insert(mClientHints
.begin(), mClientHints
.end());
244 // put action/operation (sint32) into hints
245 hints
.insert(AuthItemRef(AGENT_HINT_ACL_TAG
, AuthValueOverlay(sizeof(action
), static_cast<SInt32
*>(&action
))));
247 // item name into hints
249 hints
.insert(AuthItemRef(AGENT_HINT_KEYCHAIN_ITEM_NAME
, AuthValueOverlay(description
? strlen(description
) : 0, const_cast<char*>(description
))));
251 // keychain name into hints
252 hints
.insert(AuthItemRef(AGENT_HINT_KEYCHAIN_PATH
, AuthValueOverlay(database
? strlen(database
) : 0, const_cast<char*>(database
))));
255 if (mPassphraseCheck
)
257 status
= create("builtin", "confirm-access-password", NULL
);
259 CssmAutoData
data(Allocator::standard(Allocator::sensitive
));
264 AuthItemRef
triesHint(AGENT_HINT_TRIES
, AuthValueOverlay(sizeof(retryCount
), &retryCount
));
265 hints
.erase(triesHint
); hints
.insert(triesHint
); // replace
267 if (retryCount
++ > kMaximumAuthorizationTries
)
269 reason
= SecurityAgent::tooManyTries
;
272 AuthItemRef
retryHint(AGENT_HINT_RETRY_REASON
, AuthValueOverlay(sizeof(reason
), &reason
));
273 hints
.erase(retryHint
); hints
.insert(retryHint
); // replace
275 status
= invoke(arguments
, hints
, context
);
277 if (retryCount
> kMaximumAuthorizationTries
)
284 AuthItem
*passwordItem
= mContext
.find(kAuthorizationEnvironmentPassword
);
288 passwordItem
->getCssmData(data
);
290 while (reason
= (const_cast<KeychainDatabase
*>(mPassphraseCheck
)->decode(data
) ? SecurityAgent::noReason
: SecurityAgent::invalidPassphrase
));
294 create("builtin", "confirm-access", NULL
);
295 invoke(arguments
, hints
, context
);
304 // Perform code signature ACL access adjustment dialogs
306 bool QueryCodeCheck::operator () (const char *aclPath
)
309 AuthValueVector arguments
;
310 AuthItemSet hints
, context
;
313 if (getenv("NOSA")) {
316 string applicationPath
;
317 AuthItem
*applicationPathItem
= mClientHints
.find(AGENT_HINT_APPLICATION_PATH
);
318 if (applicationPathItem
)
319 applicationPathItem
->getString(applicationPath
);
321 getNoSA(answer
, sizeof(answer
),
322 "Allow %s to match an ACL for %s [yn][g]? ",
323 applicationPath
.c_str(), aclPath
? aclPath
: "(unknown)");
324 allow
= answer
[0] == 'y';
325 remember
= answer
[1] == 'g';
330 // prepopulate with client hints
331 hints
.insert(mClientHints
.begin(), mClientHints
.end());
333 hints
.insert(AuthItemRef(AGENT_HINT_APPLICATION_PATH
, AuthValueOverlay(strlen(aclPath
), const_cast<char*>(aclPath
))));
335 MacOSError::check(create("builtin", "code-identity", NULL
));
337 status
= invoke(arguments
, hints
, context
);
341 // MacOSError::check(status);
343 return kAuthorizationResultAllow
== result();
348 // Obtain passphrases and submit them to the accept() method until it is accepted
349 // or we can't get another passphrase. Accept() should consume the passphrase
350 // if it is accepted. If no passphrase is acceptable, throw out of here.
352 Reason
QueryUnlock::query()
354 Reason reason
= SecurityAgent::noReason
;
356 AuthValueVector arguments
;
357 AuthItemSet hints
, context
;
358 CssmAutoData
passphrase(Allocator::standard(Allocator::sensitive
));
362 // return the passphrase
363 if (getenv("NOSA")) {
364 char passphrase_
[maxPassphraseLength
];
365 getNoSA(passphrase
, maxPassphraseLength
, "Unlock %s [<CR> to cancel]: ", database
.dbName());
366 passphrase
.copy(passphrase_
, strlen(passphrase_
));
367 return database
.decode(passphrase
) ? SecurityAgent::noReason
: SecurityAgent::invalidPassphrase
;
373 // prepopulate with client hints
375 const char *keychainPath
= database
.dbName();
376 hints
.insert(AuthItemRef(AGENT_HINT_KEYCHAIN_PATH
, AuthValueOverlay(strlen(keychainPath
), const_cast<char*>(keychainPath
))));
378 hints
.insert(mClientHints
.begin(), mClientHints
.end());
380 MacOSError::check(create("builtin", "unlock-keychain", NULL
));
384 AuthItemRef
triesHint(AGENT_HINT_TRIES
, AuthValueOverlay(sizeof(retryCount
), &retryCount
));
385 hints
.erase(triesHint
); hints
.insert(triesHint
); // replace
389 if (retryCount
> maxTries
)
391 reason
= SecurityAgent::tooManyTries
;
394 AuthItemRef
retryHint(AGENT_HINT_RETRY_REASON
, AuthValueOverlay(sizeof(reason
), &reason
));
395 hints
.erase(retryHint
); hints
.insert(retryHint
); // replace
397 status
= invoke(arguments
, hints
, context
);
399 if (retryCount
> maxTries
)
406 AuthItem
*passwordItem
= mContext
.find(kAuthorizationEnvironmentPassword
);
410 passwordItem
->getCssmData(passphrase
);
412 while (reason
= database
.decode(passphrase
) ? SecurityAgent::noReason
: SecurityAgent::invalidPassphrase
);
414 return SecurityAgent::noReason
;
419 // Get existing passphrase (unlock) Query
421 Reason
QueryUnlock::operator () ()
428 // Obtain passphrases and submit them to the accept() method until it is accepted
429 // or we can't get another passphrase. Accept() should consume the passphrase
430 // if it is accepted. If no passphrase is acceptable, throw out of here.
432 Reason
QueryNewPassphrase::query()
434 Reason reason
= initialReason
;
435 CssmAutoData
passphrase(Allocator::standard(Allocator::sensitive
));
436 CssmAutoData
oldPassphrase(Allocator::standard(Allocator::sensitive
));
439 AuthValueVector arguments
;
440 AuthItemSet hints
, context
;
445 if (getenv("NOSA")) {
446 char passphrase_
[maxPassphraseLength
];
447 getNoSA(passphrase_
, maxPassphraseLength
,
448 "New passphrase for %s (reason %d) [<CR> to cancel]: ",
449 database
.dbName(), reason
);
450 return SecurityAgent::noReason
;
456 // prepopulate with client hints
457 hints
.insert(mClientHints
.begin(), mClientHints
.end());
459 // keychain name into hints
460 hints
.insert(AuthItemRef(AGENT_HINT_KEYCHAIN_PATH
, AuthValueOverlay(database
.dbName())));
462 switch (initialReason
)
464 case SecurityAgent::newDatabase
:
465 MacOSError::check(create("builtin", "new-passphrase", NULL
));
467 case SecurityAgent::changePassphrase
:
468 MacOSError::check(create("builtin", "change-passphrase", NULL
));
476 AuthItemRef
triesHint(AGENT_HINT_TRIES
, AuthValueOverlay(sizeof(retryCount
), &retryCount
));
477 hints
.erase(triesHint
); hints
.insert(triesHint
); // replace
479 if (++retryCount
> maxTries
)
481 reason
= SecurityAgent::tooManyTries
;
484 AuthItemRef
retryHint(AGENT_HINT_RETRY_REASON
, AuthValueOverlay(sizeof(reason
), &reason
));
485 hints
.erase(retryHint
); hints
.insert(retryHint
); // replace
487 status
= invoke(arguments
, hints
, context
);
489 if (retryCount
> maxTries
)
496 if (SecurityAgent::changePassphrase
== initialReason
)
498 AuthItem
*oldPasswordItem
= mContext
.find(AGENT_PASSWORD
);
499 if (!oldPasswordItem
)
502 oldPasswordItem
->getCssmData(oldPassphrase
);
505 AuthItem
*passwordItem
= mContext
.find(AGENT_CONTEXT_NEW_PASSWORD
);
509 passwordItem
->getCssmData(passphrase
);
512 while (reason
= accept(passphrase
, (initialReason
== SecurityAgent::changePassphrase
) ? &oldPassphrase
.get() : NULL
));
514 return SecurityAgent::noReason
;
519 // Get new passphrase Query
521 Reason
QueryNewPassphrase::operator () (CssmOwnedData
&passphrase
)
523 if (Reason result
= query())
524 return result
; // failed
525 passphrase
= mPassphrase
;
526 return SecurityAgent::noReason
; // success
529 Reason
QueryNewPassphrase::accept(CssmManagedData
&passphrase
, CssmData
*oldPassphrase
)
531 //@@@ acceptance criteria are currently hardwired here
532 //@@@ This validation presumes ASCII - UTF8 might be more lenient
534 // if we have an old passphrase, check it
535 if (oldPassphrase
&& !database
.validatePassphrase(*oldPassphrase
))
536 return SecurityAgent::oldPassphraseWrong
;
538 // sanity check the new passphrase (but allow user override)
539 if (!(mPassphraseValid
&& passphrase
.get() == mPassphrase
)) {
540 mPassphrase
= passphrase
;
541 mPassphraseValid
= true;
542 if (mPassphrase
.length() == 0)
543 return SecurityAgent::passphraseIsNull
;
544 if (mPassphrase
.length() < 6)
545 return SecurityAgent::passphraseTooSimple
;
549 return SecurityAgent::noReason
;
553 // Get a passphrase for unspecified use
555 Reason
QueryGenericPassphrase::operator () (const char *prompt
, bool verify
,
558 return query(prompt
, verify
, passphrase
);
561 Reason
QueryGenericPassphrase::query(const char *prompt
, bool verify
,
564 Reason reason
= SecurityAgent::noReason
;
565 OSStatus status
; // not really used; remove?
566 AuthValueVector arguments
;
567 AuthItemSet hints
, context
;
570 if (getenv("NOSA")) {
572 return SecurityAgent::noReason
;
578 hints
.insert(mClientHints
.begin(), mClientHints
.end());
579 hints
.insert(AuthItemRef(AGENT_HINT_CUSTOM_PROMPT
, AuthValueOverlay(prompt
? strlen(prompt
) : 0, const_cast<char*>(prompt
))));
580 // XXX/gh defined by dmitch but no analogous hint in
581 // AuthorizationTagsPriv.h:
582 // CSSM_ATTRIBUTE_ALERT_TITLE (optional alert panel title)
584 if (false == verify
) { // import
585 MacOSError::check(create("builtin", "generic-unlock", NULL
));
586 } else { // verify passphrase (export)
587 // new-passphrase-generic works with the pre-4 June 2004 agent;
588 // generic-new-passphrase is required for the new agent
589 MacOSError::check(create("builtin", "generic-new-passphrase", NULL
));
592 AuthItem
*passwordItem
;
596 status
= invoke(arguments
, hints
, context
);
598 passwordItem
= mContext
.find(AGENT_PASSWORD
);
600 } while (!passwordItem
);
602 passwordItem
->getString(passphrase
);
608 QueryInvokeMechanism::QueryInvokeMechanism() :
609 SecurityAgentQuery() { }
611 QueryInvokeMechanism::QueryInvokeMechanism(uid_t clientUID
, const Session
&session
, const char *agentName
) :
612 SecurityAgentQuery(clientUID
, session
, agentName
)
616 void QueryInvokeMechanism::initialize(const string
&inPluginId
, const string
&inMechanismId
, const SessionId inSessionId
)
621 MacOSError::check(create(inPluginId
.c_str(), inMechanismId
.c_str(), inSessionId
));
624 // XXX/cs should return AuthorizationResult
625 void QueryInvokeMechanism::run(const AuthValueVector
&inArguments
, AuthItemSet
&inHints
, AuthItemSet
&inContext
, AuthorizationResult
*outResult
)
627 // prepopulate with client hints
628 inHints
.insert(mClientHints
.begin(), mClientHints
.end());
630 MacOSError::check(invoke(inArguments
, inHints
, inContext
));
632 if (outResult
) *outResult
= result();
635 inContext
= context();
638 void QueryInvokeMechanism::terminateAgent()