]>
Commit | Line | Data |
---|---|---|
b1ab9ed8 A |
1 | /* |
2 | * Copyright (c) 2000-2004 Apple Computer, Inc. All Rights Reserved. | |
3 | * | |
4 | * @APPLE_LICENSE_HEADER_START@ | |
5 | * | |
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 | |
11 | * file. | |
12 | * | |
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. | |
20 | * | |
21 | * @APPLE_LICENSE_HEADER_END@ | |
22 | */ | |
23 | ||
24 | ||
25 | /* | |
26 | DLDBListCFPref.cpp | |
27 | */ | |
28 | ||
29 | #include "DLDBListCFPref.h" | |
30 | #include <Security/cssmapple.h> | |
31 | #include <security_utilities/debugging.h> | |
32 | #include <security_utilities/utilities.h> | |
33 | #include <memory> | |
34 | #include <fcntl.h> | |
35 | #include <sys/types.h> | |
36 | #include <sys/stat.h> | |
37 | #include <unistd.h> | |
38 | #include <pwd.h> | |
39 | #include <sys/param.h> | |
40 | #include <copyfile.h> | |
41 | #include <xpc/private.h> | |
42 | #include <syslog.h> | |
43 | #include <sandbox.h> | |
44 | ||
45 | dispatch_once_t AppSandboxChecked; | |
46 | xpc_object_t KeychainHomeFromXPC; | |
47 | ||
48 | using namespace CssmClient; | |
49 | ||
50 | static const double kDLDbListCFPrefRevertInterval = 30.0; | |
51 | ||
52 | // normal debug calls, which get stubbed out for deployment builds | |
53 | ||
54 | #define kKeyGUID CFSTR("GUID") | |
55 | #define kKeySubserviceId CFSTR("SubserviceId") | |
56 | #define kKeySubserviceType CFSTR("SubserviceType") | |
57 | #define kKeyDbName CFSTR("DbName") | |
58 | #define kKeyDbLocation CFSTR("DbLocation") | |
59 | #define kKeyActive CFSTR("Active") | |
60 | #define kKeyMajorVersion CFSTR("MajorVersion") | |
61 | #define kKeyMinorVersion CFSTR("MinorVersion") | |
62 | #define kDefaultDLDbListKey CFSTR("DLDBSearchList") | |
63 | #define kDefaultKeychainKey CFSTR("DefaultKeychain") | |
64 | #define kLoginKeychainKey CFSTR("LoginKeychain") | |
65 | #define kUserDefaultPath "~/Library/Preferences/com.apple.security.plist" | |
66 | #define kSystemDefaultPath "/Library/Preferences/com.apple.security.plist" | |
67 | #define kCommonDefaultPath "/Library/Preferences/com.apple.security-common.plist" | |
68 | #define kLoginKeychainPathPrefix "~/Library/Keychains/" | |
69 | #define kUserLoginKeychainPath "~/Library/Keychains/login.keychain" | |
70 | #define kSystemLoginKeychainPath "/Library/Keychains/System.keychain" | |
71 | ||
72 | ||
73 | // A utility class for managing password database lookups | |
74 | ||
75 | const time_t kPasswordCacheExpire = 30; // number of seconds cached password db info is valid | |
76 | ||
77 | PasswordDBLookup::PasswordDBLookup () : mValid (false), mCurrent (0), mTime (0) | |
78 | { | |
79 | } | |
80 | ||
81 | void PasswordDBLookup::lookupInfoOnUID (uid_t uid) | |
82 | { | |
83 | time_t currentTime = time (NULL); | |
84 | ||
85 | if (!mValid || uid != mCurrent || currentTime - mTime >= kPasswordCacheExpire) | |
86 | { | |
87 | struct passwd* pw = getpwuid(uid); | |
88 | if (pw == NULL) | |
89 | { | |
90 | UnixError::throwMe (EPERM); | |
91 | } | |
92 | ||
93 | mDirectory = pw->pw_dir; | |
94 | mName = pw->pw_name; | |
95 | mValid = true; | |
96 | mCurrent = uid; | |
97 | mTime = currentTime; | |
98 | ||
99 | secdebug("secpref", "uid=%d caching home=%s", uid, pw->pw_dir); | |
100 | ||
101 | endpwent(); | |
102 | } | |
103 | } | |
104 | ||
105 | PasswordDBLookup *DLDbListCFPref::mPdbLookup = NULL; | |
106 | ||
107 | //------------------------------------------------------------------------------------- | |
108 | // | |
109 | // Lists of DL/DBs, with CFPreferences backing store | |
110 | // | |
111 | //------------------------------------------------------------------------------------- | |
112 | ||
113 | DLDbListCFPref::DLDbListCFPref(SecPreferencesDomain domain) : mDomain(domain), mPropertyList(NULL), mChanged(false), | |
114 | mSearchListSet(false), mDefaultDLDbIdentifierSet(false), mLoginDLDbIdentifierSet(false) | |
115 | { | |
116 | secdebug("secpref", "New DLDbListCFPref %p for domain %d", this, domain); | |
117 | loadPropertyList(true); | |
118 | } | |
119 | ||
120 | void DLDbListCFPref::set(SecPreferencesDomain domain) | |
121 | { | |
122 | save(); | |
123 | ||
124 | mDomain = domain; | |
125 | ||
126 | secdebug("secpref", "DLDbListCFPref %p domain set to %d", this, domain); | |
127 | ||
128 | if (loadPropertyList(true)) | |
129 | resetCachedValues(); | |
130 | } | |
131 | ||
132 | DLDbListCFPref::~DLDbListCFPref() | |
133 | { | |
134 | save(); | |
135 | ||
136 | if (mPropertyList) | |
137 | CFRelease(mPropertyList); | |
138 | } | |
139 | ||
140 | void | |
141 | DLDbListCFPref::forceUserSearchListReread() | |
142 | { | |
143 | // set mPrefsTimeStamp so that it will "expire" the next time loadPropertyList is called | |
144 | mPrefsTimeStamp = CFAbsoluteTimeGetCurrent() - kDLDbListCFPrefRevertInterval; | |
145 | } | |
146 | ||
147 | bool | |
148 | DLDbListCFPref::loadPropertyList(bool force) | |
149 | { | |
150 | string prefsPath; | |
151 | ||
152 | switch (mDomain) | |
153 | { | |
154 | case kSecPreferencesDomainUser: | |
155 | prefsPath = ExpandTildesInPath(kUserDefaultPath); | |
156 | break; | |
157 | case kSecPreferencesDomainSystem: | |
158 | prefsPath = kSystemDefaultPath; | |
159 | break; | |
160 | case kSecPreferencesDomainCommon: | |
161 | prefsPath = kCommonDefaultPath; | |
162 | break; | |
163 | default: | |
164 | MacOSError::throwMe(errSecInvalidPrefsDomain); | |
165 | } | |
166 | ||
167 | secdebug("secpref", "force=%s prefsPath=%s", force ? "true" : "false", | |
168 | prefsPath.c_str()); | |
169 | ||
170 | CFAbsoluteTime now = CFAbsoluteTimeGetCurrent(); | |
171 | ||
172 | // If for some reason the prefs file path has changed, blow away the old plist and force an update | |
173 | if (mPrefsPath != prefsPath) | |
174 | { | |
175 | mPrefsPath = prefsPath; | |
176 | if (mPropertyList) | |
177 | { | |
178 | CFRelease(mPropertyList); | |
179 | mPropertyList = NULL; | |
180 | } | |
181 | ||
182 | mPrefsTimeStamp = now; | |
183 | } | |
184 | else if (!force) | |
185 | { | |
186 | if (now - mPrefsTimeStamp < kDLDbListCFPrefRevertInterval) | |
187 | return false; | |
188 | ||
189 | mPrefsTimeStamp = now; | |
190 | } | |
191 | ||
192 | struct stat st; | |
193 | if (stat(mPrefsPath.c_str(), &st)) | |
194 | { | |
195 | if (errno == ENOENT) | |
196 | { | |
197 | if (mPropertyList) | |
198 | { | |
199 | if (CFDictionaryGetCount(mPropertyList) == 0) | |
200 | return false; | |
201 | CFRelease(mPropertyList); | |
202 | } | |
203 | ||
204 | mPropertyList = CFDictionaryCreateMutable(kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); | |
205 | return true; | |
206 | } | |
207 | } | |
208 | else | |
209 | { | |
210 | if (mPropertyList) | |
211 | { | |
212 | if (mTimespec.tv_sec == st.st_mtimespec.tv_sec | |
213 | && mTimespec.tv_nsec == st.st_mtimespec.tv_nsec) | |
214 | return false; | |
215 | } | |
216 | ||
217 | mTimespec = st.st_mtimespec; | |
218 | } | |
219 | ||
220 | CFMutableDictionaryRef thePropertyList = NULL; | |
221 | CFMutableDataRef xmlData = NULL; | |
222 | CFStringRef errorString = NULL; | |
223 | int fd = -1; | |
224 | ||
225 | do | |
226 | { | |
227 | fd = open(mPrefsPath.c_str(), O_RDONLY, 0); | |
228 | if (fd < 0) | |
229 | break; | |
230 | ||
231 | off_t theSize = lseek(fd, 0, SEEK_END); | |
232 | if (theSize <= 0) | |
233 | break; | |
234 | ||
235 | if (lseek(fd, 0, SEEK_SET)) | |
236 | break; | |
237 | ||
238 | xmlData = CFDataCreateMutable(NULL, CFIndex(theSize)); | |
239 | if (!xmlData) | |
240 | break; | |
241 | CFDataSetLength(xmlData, CFIndex(theSize)); | |
242 | void *buffer = reinterpret_cast<void *>(CFDataGetMutableBytePtr(xmlData)); | |
243 | if (!buffer) | |
244 | break; | |
427c49bc | 245 | ssize_t bytesRead = read(fd, buffer, (size_t)theSize); |
b1ab9ed8 A |
246 | if (bytesRead != theSize) |
247 | break; | |
248 | ||
249 | thePropertyList = CFMutableDictionaryRef(CFPropertyListCreateFromXMLData(NULL, xmlData, kCFPropertyListMutableContainers, &errorString)); | |
250 | if (!thePropertyList) | |
251 | break; | |
252 | ||
253 | if (CFGetTypeID(thePropertyList) != CFDictionaryGetTypeID()) | |
254 | { | |
255 | CFRelease(thePropertyList); | |
256 | thePropertyList = NULL; | |
257 | break; | |
258 | } | |
259 | } while (0); | |
260 | ||
261 | if (fd >= 0) | |
262 | close(fd); | |
263 | if (xmlData) | |
264 | CFRelease(xmlData); | |
265 | if (errorString) | |
266 | CFRelease(errorString); | |
267 | ||
268 | if (!thePropertyList) | |
269 | { | |
270 | thePropertyList = CFDictionaryCreateMutable(kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); | |
271 | } | |
272 | ||
273 | if (mPropertyList) | |
274 | { | |
275 | if (CFEqual(mPropertyList, thePropertyList)) | |
276 | { | |
277 | // The new property list is the same as the old one, so nothing has changed. | |
278 | CFRelease(thePropertyList); | |
279 | return false; | |
280 | } | |
281 | CFRelease(mPropertyList); | |
282 | } | |
283 | ||
284 | mPropertyList = thePropertyList; | |
285 | return true; | |
286 | } | |
287 | ||
288 | void | |
289 | DLDbListCFPref::writePropertyList() | |
290 | { | |
291 | if (!mPropertyList || CFDictionaryGetCount(mPropertyList) == 0) | |
292 | { | |
293 | // There is nothing in the mPropertyList dictionary, | |
294 | // so we don't need a prefs file. | |
295 | unlink(mPrefsPath.c_str()); | |
296 | } | |
297 | else | |
298 | { | |
299 | if(testAndFixPropertyList()) | |
300 | return; | |
301 | ||
302 | CFDataRef xmlData = CFPropertyListCreateXMLData(NULL, mPropertyList); | |
303 | if (!xmlData) | |
304 | return; // Bad out of memory or something evil happened let's act like CF and do nothing. | |
305 | ||
306 | // The prefs file should at least be made readable by user/group/other and writable by the owner. | |
307 | // Change from euid to ruid if needed for the duration of the new prefs file creat. | |
308 | ||
309 | mode_t mode = 0666; | |
310 | changeIdentity(UNPRIV); | |
311 | int fd = open(mPrefsPath.c_str(), O_WRONLY|O_CREAT|O_TRUNC, mode); | |
312 | changeIdentity(PRIV); | |
313 | if (fd >= 0) | |
314 | { | |
315 | const void *buffer = CFDataGetBytePtr(xmlData); | |
316 | size_t toWrite = CFDataGetLength(xmlData); | |
317 | /* ssize_t bytesWritten = */ write(fd, buffer, toWrite); | |
318 | // Emulate CFPreferences by not checking for any errors. | |
319 | ||
320 | fsync(fd); | |
321 | struct stat st; | |
322 | if (!fstat(fd, &st)) | |
323 | mTimespec = st.st_mtimespec; | |
324 | ||
325 | close(fd); | |
326 | } | |
327 | ||
328 | CFRelease(xmlData); | |
329 | } | |
330 | ||
331 | mPrefsTimeStamp = CFAbsoluteTimeGetCurrent(); | |
332 | } | |
333 | ||
334 | // This function can clean up some problems caused by setuid clients. We've had instances where the | |
335 | // Keychain search list has become owned by root, but is still able to be re-written by the user because | |
336 | // of the permissions on the directory above. We'll take advantage of that fact to recreate the file with | |
337 | // the correct ownership by copying it. | |
338 | ||
339 | int | |
340 | DLDbListCFPref::testAndFixPropertyList() | |
341 | { | |
342 | char *prefsPath = (char *)mPrefsPath.c_str(); | |
343 | ||
344 | int fd1, fd2, retval; | |
345 | struct stat stbuf; | |
346 | ||
347 | if((fd1 = open(prefsPath, O_RDONLY)) < 0) { | |
348 | if (errno == ENOENT) return 0; // Doesn't exist - the default case | |
349 | else return -1; | |
350 | } | |
351 | ||
352 | if((retval = fstat(fd1, &stbuf)) == -1) return -1; | |
353 | ||
354 | if(stbuf.st_uid != getuid()) { | |
355 | char tempfile[MAXPATHLEN+1]; | |
356 | ||
357 | snprintf(tempfile, MAXPATHLEN, "%s.XXXXX", prefsPath); | |
358 | mktemp(tempfile); | |
359 | changeIdentity(UNPRIV); | |
360 | if((fd2 = open(tempfile, O_RDWR | O_CREAT | O_EXCL, 0666)) < 0) { | |
361 | retval = -1; | |
362 | } else { | |
363 | copyfile_state_t s = copyfile_state_alloc(); | |
364 | retval = fcopyfile(fd1, fd2, s, COPYFILE_DATA); | |
365 | copyfile_state_free(s); | |
366 | if(!retval) retval = ::unlink(prefsPath); | |
367 | if(!retval) retval = ::rename(tempfile, prefsPath); | |
368 | } | |
369 | changeIdentity(PRIV); | |
370 | close(fd2); | |
371 | } | |
372 | close(fd1); | |
373 | return retval; | |
374 | } | |
375 | ||
376 | // Encapsulated process uid/gid change routine. | |
377 | void | |
378 | DLDbListCFPref::changeIdentity(ID_Direction toPriv) | |
379 | { | |
380 | if(toPriv == UNPRIV) { | |
381 | savedEUID = geteuid(); | |
382 | savedEGID = getegid(); | |
383 | if(savedEGID != getgid()) setegid(getgid()); | |
384 | if(savedEUID != getuid()) seteuid(getuid()); | |
385 | } else { | |
386 | if(savedEUID != getuid()) seteuid(savedEUID); | |
387 | if(savedEGID != getgid()) setegid(savedEGID); | |
388 | } | |
389 | } | |
390 | ||
391 | void | |
392 | DLDbListCFPref::resetCachedValues() | |
393 | { | |
394 | // Unset the login and default Keychain. | |
395 | mLoginDLDbIdentifier = mDefaultDLDbIdentifier = DLDbIdentifier(); | |
396 | ||
397 | // Clear the searchList. | |
398 | mSearchList.clear(); | |
399 | ||
400 | changed(false); | |
401 | ||
402 | // Note that none of our cached values are valid | |
403 | mSearchListSet = mDefaultDLDbIdentifierSet = mLoginDLDbIdentifierSet = false; | |
404 | ||
405 | mPrefsTimeStamp = CFAbsoluteTimeGetCurrent(); | |
406 | } | |
407 | ||
408 | void DLDbListCFPref::save() | |
409 | { | |
410 | if (!hasChanged()) | |
411 | return; | |
412 | ||
413 | // Resync from disc to make sure we don't clobber anyone elses changes. | |
414 | // @@@ This is probably already done by the next layer up so we don't | |
415 | // really need to do it here again. | |
416 | loadPropertyList(true); | |
417 | ||
418 | // Do the searchList first since it might end up invoking defaultDLDbIdentifier() which can set | |
419 | // mLoginDLDbIdentifierSet and mDefaultDLDbIdentifierSet to true. | |
420 | if (mSearchListSet) | |
421 | { | |
422 | // Make a temporary CFArray with the contents of the vector | |
423 | if (mSearchList.size() == 1 && mSearchList[0] == defaultDLDbIdentifier() && mSearchList[0] == LoginDLDbIdentifier()) | |
424 | { | |
425 | // The only element in the search list is the default keychain, which is a | |
426 | // post Jaguar style login keychain, so omit the entry from the prefs file. | |
427 | CFDictionaryRemoveValue(mPropertyList, kDefaultDLDbListKey); | |
428 | } | |
429 | else | |
430 | { | |
431 | CFMutableArrayRef searchArray = CFArrayCreateMutable(kCFAllocatorDefault, mSearchList.size(), &kCFTypeArrayCallBacks); | |
432 | for (DLDbList::const_iterator ix=mSearchList.begin();ix!=mSearchList.end();ix++) | |
433 | { | |
434 | CFDictionaryRef aDict = dlDbIdentifierToCFDictionaryRef(*ix); | |
435 | CFArrayAppendValue(searchArray, aDict); | |
436 | CFRelease(aDict); | |
437 | } | |
438 | ||
439 | CFDictionarySetValue(mPropertyList, kDefaultDLDbListKey, searchArray); | |
440 | CFRelease(searchArray); | |
441 | } | |
442 | } | |
443 | ||
444 | if (mLoginDLDbIdentifierSet) | |
445 | { | |
446 | // Make a temporary CFArray with the login keychain | |
447 | CFArrayRef loginArray = NULL; | |
448 | if (!mLoginDLDbIdentifier) | |
449 | { | |
450 | loginArray = CFArrayCreate(kCFAllocatorDefault, NULL, 0, &kCFTypeArrayCallBacks); | |
451 | } | |
452 | else if (!(mLoginDLDbIdentifier == LoginDLDbIdentifier())) | |
453 | { | |
454 | CFDictionaryRef aDict = dlDbIdentifierToCFDictionaryRef(mLoginDLDbIdentifier); | |
455 | const void *value = reinterpret_cast<const void *>(aDict); | |
456 | loginArray = CFArrayCreate(kCFAllocatorDefault, &value, 1, &kCFTypeArrayCallBacks); | |
457 | CFRelease(aDict); | |
458 | } | |
459 | ||
460 | if (loginArray) | |
461 | { | |
462 | CFDictionarySetValue(mPropertyList, kLoginKeychainKey, loginArray); | |
463 | CFRelease(loginArray); | |
464 | } | |
465 | else | |
466 | CFDictionaryRemoveValue(mPropertyList, kLoginKeychainKey); | |
467 | } | |
468 | ||
469 | if (mDefaultDLDbIdentifierSet) | |
470 | { | |
471 | // Make a temporary CFArray with the default keychain | |
472 | CFArrayRef defaultArray = NULL; | |
473 | if (!mDefaultDLDbIdentifier) | |
474 | { | |
475 | defaultArray = CFArrayCreate(kCFAllocatorDefault, NULL, 0, &kCFTypeArrayCallBacks); | |
476 | } | |
477 | else if (!(mDefaultDLDbIdentifier == LoginDLDbIdentifier())) | |
478 | { | |
479 | CFDictionaryRef aDict = dlDbIdentifierToCFDictionaryRef(mDefaultDLDbIdentifier); | |
480 | const void *value = reinterpret_cast<const void *>(aDict); | |
481 | defaultArray = CFArrayCreate(kCFAllocatorDefault, &value, 1, &kCFTypeArrayCallBacks); | |
482 | CFRelease(aDict); | |
483 | } | |
484 | ||
485 | if (defaultArray) | |
486 | { | |
487 | CFDictionarySetValue(mPropertyList, kDefaultKeychainKey, defaultArray); | |
488 | CFRelease(defaultArray); | |
489 | } | |
490 | else | |
491 | CFDictionaryRemoveValue(mPropertyList, kDefaultKeychainKey); | |
492 | } | |
493 | ||
494 | writePropertyList(); | |
495 | changed(false); | |
496 | } | |
497 | ||
498 | ||
499 | //---------------------------------------------------------------------- | |
500 | // Conversions | |
501 | //---------------------------------------------------------------------- | |
502 | ||
503 | DLDbIdentifier DLDbListCFPref::LoginDLDbIdentifier() | |
504 | { | |
505 | CSSM_VERSION theVersion={}; | |
506 | CssmSubserviceUid ssuid(gGuidAppleCSPDL,&theVersion,0,CSSM_SERVICE_DL|CSSM_SERVICE_CSP); | |
507 | CssmNetAddress *dbLocation=NULL; | |
508 | ||
509 | switch (mDomain) { | |
510 | case kSecPreferencesDomainUser: | |
511 | return DLDbIdentifier(ssuid, ExpandTildesInPath(kUserLoginKeychainPath).c_str(), dbLocation); | |
512 | default: | |
513 | assert(false); | |
514 | case kSecPreferencesDomainSystem: | |
515 | case kSecPreferencesDomainCommon: | |
516 | return DLDbIdentifier(ssuid, kSystemLoginKeychainPath, dbLocation); | |
517 | } | |
518 | } | |
519 | ||
520 | DLDbIdentifier DLDbListCFPref::JaguarLoginDLDbIdentifier() | |
521 | { | |
522 | CSSM_VERSION theVersion={}; | |
523 | CssmSubserviceUid ssuid(gGuidAppleCSPDL,&theVersion,0,CSSM_SERVICE_DL|CSSM_SERVICE_CSP); | |
524 | CssmNetAddress *dbLocation=NULL; | |
525 | ||
526 | switch (mDomain) { | |
527 | case kSecPreferencesDomainUser: | |
528 | { | |
529 | string basepath = ExpandTildesInPath(kLoginKeychainPathPrefix) + getPwInfo(kUsername); | |
530 | return DLDbIdentifier(ssuid,basepath.c_str(),dbLocation); | |
531 | } | |
532 | case kSecPreferencesDomainSystem: | |
533 | case kSecPreferencesDomainCommon: | |
534 | return DLDbIdentifier(ssuid, kSystemLoginKeychainPath, dbLocation); | |
535 | default: | |
536 | assert(false); | |
537 | return DLDbIdentifier(); | |
538 | } | |
539 | } | |
540 | ||
541 | DLDbIdentifier DLDbListCFPref::makeDLDbIdentifier (const CSSM_GUID &guid, const CSSM_VERSION &version, | |
542 | uint32 subserviceId, CSSM_SERVICE_TYPE subserviceType, | |
543 | const char* dbName, CSSM_NET_ADDRESS *dbLocation) | |
544 | { | |
545 | CssmSubserviceUid ssuid (guid, &version, subserviceId, subserviceType); | |
546 | return DLDbIdentifier (ssuid, ExpandTildesInPath (dbName).c_str (), dbLocation); | |
547 | } | |
548 | ||
549 | DLDbIdentifier DLDbListCFPref::cfDictionaryRefToDLDbIdentifier(CFDictionaryRef theDict) | |
550 | { | |
551 | // We must get individual values from the dictionary and store in basic types | |
552 | if (CFGetTypeID(theDict) != CFDictionaryGetTypeID()) | |
553 | throw std::logic_error("wrong type in property list"); | |
554 | ||
555 | // GUID | |
556 | CCFValue vGuid(::CFDictionaryGetValue(theDict,kKeyGUID)); | |
557 | string guidStr=vGuid; | |
558 | const Guid guid(guidStr.c_str()); | |
559 | ||
560 | //CSSM_VERSION | |
561 | CSSM_VERSION theVersion={0,}; | |
562 | CCFValue vMajor(::CFDictionaryGetValue(theDict,kKeyMajorVersion)); | |
563 | theVersion.Major = vMajor; | |
564 | CCFValue vMinor(::CFDictionaryGetValue(theDict,kKeyMinorVersion)); | |
565 | theVersion.Minor = vMinor; | |
566 | ||
567 | //subserviceId | |
568 | CCFValue vSsid(::CFDictionaryGetValue(theDict,kKeySubserviceId)); | |
569 | uint32 subserviceId=sint32(vSsid); | |
570 | ||
571 | //CSSM_SERVICE_TYPE | |
572 | CSSM_SERVICE_TYPE subserviceType=CSSM_SERVICE_DL; | |
573 | CCFValue vSsType(::CFDictionaryGetValue(theDict,kKeySubserviceType)); | |
574 | subserviceType=vSsType; | |
575 | ||
576 | // Get DbName from dictionary | |
577 | CCFValue vDbName(::CFDictionaryGetValue(theDict,kKeyDbName)); | |
578 | string dbName=vDbName; | |
579 | ||
580 | // jch Get DbLocation from dictionary | |
581 | CssmNetAddress *dbLocation=NULL; | |
582 | ||
583 | return makeDLDbIdentifier (guid, theVersion, subserviceId, subserviceType, dbName.c_str (), dbLocation); | |
584 | } | |
585 | ||
586 | void DLDbListCFPref::clearPWInfo () | |
587 | { | |
588 | if (mPdbLookup != NULL) | |
589 | { | |
590 | delete mPdbLookup; | |
591 | mPdbLookup = NULL; | |
592 | } | |
593 | } | |
594 | ||
595 | string DLDbListCFPref::getPwInfo(PwInfoType type) | |
596 | { | |
597 | const char *value; | |
598 | switch (type) | |
599 | { | |
600 | case kHomeDir: | |
601 | if (KeychainHomeFromXPC) { | |
602 | value = xpc_string_get_string_ptr(KeychainHomeFromXPC); | |
603 | } else { | |
604 | value = getenv("HOME"); | |
605 | } | |
606 | if (value) | |
607 | return value; | |
608 | break; | |
609 | case kUsername: | |
610 | value = getenv("USER"); | |
611 | if (value) | |
612 | return value; | |
613 | break; | |
614 | } | |
615 | ||
616 | // Get our effective uid | |
617 | uid_t uid = geteuid(); | |
618 | // If we are setuid root use the real uid instead | |
619 | if (!uid) uid = getuid(); | |
620 | ||
621 | // get the password entries | |
622 | if (mPdbLookup == NULL) | |
623 | { | |
624 | mPdbLookup = new PasswordDBLookup (); | |
625 | } | |
626 | ||
627 | mPdbLookup->lookupInfoOnUID (uid); | |
628 | ||
629 | string result; | |
630 | switch (type) | |
631 | { | |
632 | case kHomeDir: | |
633 | result = mPdbLookup->getDirectory (); | |
634 | break; | |
635 | case kUsername: | |
636 | result = mPdbLookup->getName (); | |
637 | break; | |
638 | } | |
639 | ||
640 | return result; | |
641 | } | |
642 | ||
643 | static void check_app_sandbox() | |
644 | { | |
645 | if (!_xpc_runtime_is_app_sandboxed()) { | |
646 | // We are not in a sandbox, no work to do here | |
647 | return; | |
648 | } | |
649 | ||
650 | extern xpc_object_t xpc_create_with_format(const char * format, ...); | |
651 | xpc_connection_t con = xpc_connection_create("com.apple.security.XPCKeychainSandboxCheck", NULL); | |
652 | xpc_connection_set_event_handler(con, ^(xpc_object_t event) { | |
653 | xpc_type_t xtype = xpc_get_type(event); | |
654 | if (XPC_TYPE_ERROR == xtype) { | |
655 | syslog(LOG_ERR, "Keychain sandbox connection error: %s\n", xpc_dictionary_get_string(event, XPC_ERROR_KEY_DESCRIPTION)); | |
656 | } else { | |
657 | syslog(LOG_ERR, "Keychain sandbox unexpected connection event %p\n", event); | |
658 | } | |
659 | }); | |
660 | xpc_connection_resume(con); | |
661 | ||
662 | xpc_object_t message = xpc_create_with_format("{op: GrantKeychainPaths}"); | |
663 | xpc_object_t reply = xpc_connection_send_message_with_reply_sync(con, message); | |
664 | xpc_type_t xtype = xpc_get_type(reply); | |
665 | if (XPC_TYPE_DICTIONARY == xtype) { | |
666 | #if 0 | |
667 | // This is useful for debugging. | |
668 | char *debug = xpc_copy_description(reply); | |
669 | syslog(LOG_ERR, "DEBUG (KCsandbox) %s\n", debug); | |
670 | free(debug); | |
671 | #endif | |
672 | ||
673 | xpc_object_t extensions_array = xpc_dictionary_get_value(reply, "extensions"); | |
674 | xpc_array_apply(extensions_array, ^(size_t index, xpc_object_t extension) { | |
675 | char pbuf[MAXPATHLEN]; | |
676 | char *path = pbuf; | |
677 | int status = sandbox_consume_fs_extension(xpc_string_get_string_ptr(extension), &path); | |
678 | if (status) { | |
679 | syslog(LOG_ERR, "Keychain sandbox consume extension error: s=%d p=%s %m\n", status, path); | |
680 | } | |
427c49bc A |
681 | status = sandbox_release_fs_extension(xpc_string_get_string_ptr(extension)); |
682 | if (status) { | |
683 | syslog(LOG_ERR, "Keychain sandbox release extension error: s=%d p=%s %m\n", status, path); | |
684 | } | |
685 | ||
b1ab9ed8 A |
686 | return (bool)true; |
687 | }); | |
688 | ||
689 | KeychainHomeFromXPC = xpc_dictionary_get_value(reply, "keychain-home"); | |
690 | xpc_retain(KeychainHomeFromXPC); | |
691 | xpc_release(con); | |
692 | } else if (XPC_TYPE_ERROR == xtype) { | |
693 | syslog(LOG_ERR, "Keychain sandbox message error: %s\n", xpc_dictionary_get_string(reply, XPC_ERROR_KEY_DESCRIPTION)); | |
694 | } else { | |
695 | syslog(LOG_ERR, "Keychain sandbox unexpected message reply type %p\n", xtype); | |
696 | } | |
697 | xpc_release(message); | |
698 | xpc_release(reply); | |
699 | } | |
700 | ||
701 | ||
702 | ||
703 | string DLDbListCFPref::ExpandTildesInPath(const string &inPath) | |
704 | { | |
705 | dispatch_once(&AppSandboxChecked, ^{ | |
706 | check_app_sandbox(); | |
707 | }); | |
708 | ||
709 | if ((short)inPath.find("~/",0,2) == 0) | |
427c49bc | 710 | return getPwInfo(kHomeDir) + inPath.substr(1, inPath.length() - 1); |
b1ab9ed8 A |
711 | else |
712 | return inPath; | |
713 | } | |
714 | ||
715 | string DLDbListCFPref::StripPathStuff(const string &inPath) | |
716 | { | |
717 | if (inPath.find("/private/var/automount/Network/",0,31) == 0) | |
718 | return inPath.substr(22); | |
719 | if (inPath.find("/private/automount/Servers/",0,27) == 0) | |
720 | return "/Network" + inPath.substr(18); | |
721 | if (inPath.find("/automount/Servers/",0,19) == 0) | |
722 | return "/Network" + inPath.substr(10); | |
723 | if (inPath.find("/private/automount/Network/",0,27) == 0) | |
724 | return inPath.substr(18); | |
725 | if (inPath.find("/automount/Network/",0,19) == 0) | |
726 | return inPath.substr(10); | |
727 | if (inPath.find("/private/Network/",0,17) == 0) | |
728 | return inPath.substr(8); | |
729 | return inPath; | |
730 | } | |
731 | ||
732 | string DLDbListCFPref::AbbreviatedPath(const string &inPath) | |
733 | { | |
734 | string path = StripPathStuff(inPath); | |
735 | string home = StripPathStuff(getPwInfo(kHomeDir) + "/"); | |
736 | size_t homeLen = home.length(); | |
737 | ||
738 | if (homeLen > 1 && path.find(home.c_str(), 0, homeLen) == 0) | |
739 | return "~" + path.substr(homeLen - 1); | |
740 | else | |
741 | return path; | |
742 | } | |
743 | ||
744 | CFDictionaryRef DLDbListCFPref::dlDbIdentifierToCFDictionaryRef(const DLDbIdentifier& dldbIdentifier) | |
745 | { | |
746 | CFRef<CFMutableDictionaryRef> aDict(CFDictionaryCreateMutable(kCFAllocatorDefault,0, | |
747 | &kCFTypeDictionaryKeyCallBacks,&kCFTypeDictionaryValueCallBacks)); | |
748 | if (!aDict) | |
749 | throw ::std::bad_alloc(); | |
750 | ||
751 | // Put SUBSERVICE_UID in dictionary | |
752 | char buffer[Guid::stringRepLength+1]; | |
753 | const CssmSubserviceUid& ssuid=dldbIdentifier.ssuid(); | |
754 | const Guid &theGuid = Guid::overlay(ssuid.Guid); | |
755 | CFRef<CFStringRef> stringGuid(::CFStringCreateWithCString(kCFAllocatorDefault, | |
756 | theGuid.toString(buffer),kCFStringEncodingMacRoman)); | |
757 | if (stringGuid) | |
758 | ::CFDictionarySetValue(aDict,kKeyGUID,stringGuid); | |
759 | ||
760 | if (ssuid.SubserviceId!=0) | |
761 | { | |
762 | CFRef<CFNumberRef> subserviceId(::CFNumberCreate(kCFAllocatorDefault,kCFNumberSInt32Type,&ssuid.SubserviceId)); | |
763 | if (subserviceId) | |
764 | ::CFDictionarySetValue(aDict,kKeySubserviceId,subserviceId); | |
765 | } | |
766 | if (ssuid.SubserviceType!=0) | |
767 | { | |
768 | CFRef<CFNumberRef> subserviceType(CFNumberCreate(kCFAllocatorDefault,kCFNumberSInt32Type,&ssuid.SubserviceType)); | |
769 | if (subserviceType) | |
770 | ::CFDictionarySetValue(aDict,kKeySubserviceType,subserviceType); | |
771 | } | |
772 | if (ssuid.Version.Major!=0 && ssuid.Version.Minor!=0) | |
773 | { | |
774 | CFRef<CFNumberRef> majorVersion(::CFNumberCreate(kCFAllocatorDefault,kCFNumberSInt32Type,&ssuid.Version.Major)); | |
775 | if (majorVersion) | |
776 | ::CFDictionarySetValue(aDict,kKeyMajorVersion,majorVersion); | |
777 | CFRef<CFNumberRef> minorVersion(::CFNumberCreate(kCFAllocatorDefault,kCFNumberSInt32Type,&ssuid.Version.Minor)); | |
778 | if (minorVersion) | |
779 | ::CFDictionarySetValue(aDict,kKeyMinorVersion,minorVersion); | |
780 | } | |
781 | ||
782 | // Put DbName in dictionary | |
783 | const char *dbName=dldbIdentifier.dbName(); | |
784 | if (dbName) | |
785 | { | |
786 | CFRef<CFStringRef> theDbName(::CFStringCreateWithCString(kCFAllocatorDefault,AbbreviatedPath(dbName).c_str(),kCFStringEncodingUTF8)); | |
787 | ::CFDictionarySetValue(aDict,kKeyDbName,theDbName); | |
788 | } | |
789 | // Put DbLocation in dictionary | |
790 | const CSSM_NET_ADDRESS *dbLocation=dldbIdentifier.dbLocation(); | |
791 | if (dbLocation!=NULL && dbLocation->AddressType!=CSSM_ADDR_NONE) | |
792 | { | |
793 | CFRef<CFDataRef> theData(::CFDataCreate(kCFAllocatorDefault,dbLocation->Address.Data,dbLocation->Address.Length)); | |
794 | if (theData) | |
795 | ::CFDictionarySetValue(aDict,kKeyDbLocation,theData); | |
796 | } | |
797 | ||
798 | ::CFRetain(aDict); | |
799 | return aDict; | |
800 | } | |
801 | ||
802 | bool DLDbListCFPref::revert(bool force) | |
803 | { | |
804 | // If the prefs have not been refreshed in the last kDLDbListCFPrefRevertInterval | |
805 | // seconds or we are asked to force a reload, then reload. | |
806 | if (!loadPropertyList(force)) | |
807 | return false; | |
808 | ||
809 | resetCachedValues(); | |
810 | return true; | |
811 | } | |
812 | ||
813 | void | |
814 | DLDbListCFPref::add(const DLDbIdentifier &dldbIdentifier) | |
815 | { | |
816 | // convert the location specified in dldbIdentifier to a standard form | |
817 | // make a canonical form of the database name | |
818 | std::string canon = ExpandTildesInPath(AbbreviatedPath(dldbIdentifier.dbName()).c_str()); | |
819 | ||
820 | DLDbIdentifier localIdentifier (dldbIdentifier.ssuid(), canon.c_str(), dldbIdentifier.dbLocation ()); | |
821 | ||
822 | if (member(localIdentifier)) | |
823 | return; | |
824 | ||
825 | mSearchList.push_back(localIdentifier); | |
826 | changed(true); | |
827 | } | |
828 | ||
829 | void | |
830 | DLDbListCFPref::remove(const DLDbIdentifier &dldbIdentifier) | |
831 | { | |
832 | // Make sure mSearchList is set | |
833 | searchList(); | |
834 | for (vector<DLDbIdentifier>::iterator ix = mSearchList.begin(); ix != mSearchList.end(); ++ix) | |
835 | { | |
836 | if (*ix==dldbIdentifier) // found in list | |
837 | { | |
838 | mSearchList.erase(ix); | |
839 | changed(true); | |
840 | break; | |
841 | } | |
842 | } | |
843 | } | |
844 | ||
845 | void | |
846 | DLDbListCFPref::rename(const DLDbIdentifier &oldId, const DLDbIdentifier &newId) | |
847 | { | |
848 | // Make sure mSearchList is set | |
849 | searchList(); | |
850 | for (vector<DLDbIdentifier>::iterator ix = mSearchList.begin(); | |
851 | ix != mSearchList.end(); ++ix) | |
852 | { | |
853 | if (*ix==oldId) | |
854 | { | |
855 | // replace oldId with newId | |
856 | *ix = newId; | |
857 | changed(true); | |
858 | } | |
859 | else if (*ix==newId) | |
860 | { | |
861 | // remove newId except where we just inserted it | |
862 | mSearchList.erase(ix); | |
863 | changed(true); | |
864 | } | |
865 | } | |
866 | } | |
867 | ||
868 | bool | |
869 | DLDbListCFPref::member(const DLDbIdentifier &dldbIdentifier) | |
870 | { | |
c38e3ce9 A |
871 | if (dldbIdentifier.IsImplEmpty()) |
872 | { | |
873 | return false; | |
874 | } | |
875 | ||
b1ab9ed8 A |
876 | for (vector<DLDbIdentifier>::const_iterator ix = searchList().begin(); ix != mSearchList.end(); ++ix) |
877 | { | |
878 | if (ix->mImpl == NULL) | |
879 | { | |
880 | continue; | |
881 | } | |
882 | ||
883 | // compare the dldbIdentifiers based on the full, real path to the keychain | |
884 | if (ix->ssuid() == dldbIdentifier.ssuid()) | |
885 | { | |
886 | char localPath[PATH_MAX], | |
887 | inPath[PATH_MAX]; | |
888 | ||
889 | // try to resolve these down to a canonical form | |
890 | const char* localPathPtr = cached_realpath(ix->dbName(), localPath); | |
891 | const char* inPathPtr = cached_realpath(dldbIdentifier.dbName(), inPath); | |
892 | ||
893 | // if either of the paths didn't resolve for some reason, use the originals | |
894 | if (localPathPtr == NULL) | |
895 | { | |
896 | localPathPtr = ix->dbName(); | |
897 | } | |
898 | ||
899 | if (inPathPtr == NULL) | |
900 | { | |
901 | inPathPtr = dldbIdentifier.dbName(); | |
902 | } | |
903 | ||
904 | if (strcmp(localPathPtr, inPathPtr) == 0) | |
905 | { | |
906 | return true; | |
907 | } | |
908 | } | |
909 | } | |
910 | ||
911 | return false; | |
912 | } | |
913 | ||
914 | const vector<DLDbIdentifier> & | |
915 | DLDbListCFPref::searchList() | |
916 | { | |
917 | if (!mSearchListSet) | |
918 | { | |
919 | CFArrayRef searchList = reinterpret_cast<CFArrayRef>(CFDictionaryGetValue(mPropertyList, kDefaultDLDbListKey)); | |
920 | if (searchList && CFGetTypeID(searchList) != CFArrayGetTypeID()) | |
921 | searchList = NULL; | |
922 | ||
923 | if (searchList) | |
924 | { | |
925 | CFIndex top = CFArrayGetCount(searchList); | |
926 | // Each entry is a CFDictionary; peel it off & add it to the array | |
927 | for (CFIndex idx = 0; idx < top; ++idx) | |
928 | { | |
929 | CFDictionaryRef theDict = reinterpret_cast<CFDictionaryRef>(CFArrayGetValueAtIndex(searchList, idx)); | |
930 | try | |
931 | { | |
932 | mSearchList.push_back(cfDictionaryRefToDLDbIdentifier(theDict)); | |
933 | } | |
934 | catch (...) | |
935 | { | |
936 | // Drop stuff that doesn't parse on the floor. | |
937 | } | |
938 | } | |
939 | ||
940 | // If there were entries specified, but they were invalid revert to using the | |
941 | // default keychain in the searchlist. | |
942 | if (top > 0 && mSearchList.size() == 0) | |
943 | searchList = NULL; | |
944 | } | |
945 | ||
946 | // The default when no search list is specified is to only search the | |
947 | // default keychain. | |
948 | if (!searchList && static_cast<bool>(defaultDLDbIdentifier())) | |
949 | mSearchList.push_back(mDefaultDLDbIdentifier); | |
950 | ||
951 | mSearchListSet = true; | |
952 | } | |
953 | ||
954 | return mSearchList; | |
955 | } | |
956 | ||
957 | void | |
958 | DLDbListCFPref::searchList(const vector<DLDbIdentifier> &searchList) | |
959 | { | |
960 | vector<DLDbIdentifier> newList(searchList); | |
961 | mSearchList.swap(newList); | |
962 | mSearchListSet = true; | |
963 | changed(true); | |
964 | } | |
965 | ||
966 | void | |
967 | DLDbListCFPref::defaultDLDbIdentifier(const DLDbIdentifier &dlDbIdentifier) | |
968 | { | |
969 | if (!(defaultDLDbIdentifier() == dlDbIdentifier)) | |
970 | { | |
971 | mDefaultDLDbIdentifier = dlDbIdentifier; | |
972 | changed(true); | |
973 | } | |
974 | } | |
975 | ||
976 | const DLDbIdentifier & | |
977 | DLDbListCFPref::defaultDLDbIdentifier() | |
978 | { | |
979 | ||
980 | if (!mDefaultDLDbIdentifierSet) | |
981 | { | |
982 | CFArrayRef defaultArray = reinterpret_cast<CFArrayRef>(CFDictionaryGetValue(mPropertyList, kDefaultKeychainKey)); | |
983 | if (defaultArray && CFGetTypeID(defaultArray) != CFArrayGetTypeID()) | |
984 | defaultArray = NULL; | |
985 | ||
986 | if (defaultArray && CFArrayGetCount(defaultArray) > 0) | |
987 | { | |
988 | CFDictionaryRef defaultDict = reinterpret_cast<CFDictionaryRef>(CFArrayGetValueAtIndex(defaultArray, 0)); | |
989 | try | |
990 | { | |
991 | secdebug("secpref", "getting default DLDbIdentifier from defaultDict"); | |
992 | mDefaultDLDbIdentifier = cfDictionaryRefToDLDbIdentifier(defaultDict); | |
993 | secdebug("secpref", "now we think the default keychain is %s", (mDefaultDLDbIdentifier) ? mDefaultDLDbIdentifier.dbName() : "<NULL>"); | |
994 | } | |
995 | catch (...) | |
996 | { | |
997 | // If defaultArray doesn't parse fall back on the default way of getting the default keychain | |
998 | defaultArray = NULL; | |
999 | } | |
1000 | } | |
1001 | ||
1002 | if (!defaultArray) | |
1003 | { | |
1004 | ||
1005 | // If the Panther style login keychain actually exists we use that otherwise no | |
1006 | // default is set. | |
1007 | mDefaultDLDbIdentifier = loginDLDbIdentifier(); | |
1008 | secdebug("secpref", "now we think the default keychain is: %s", (mDefaultDLDbIdentifier) ? mDefaultDLDbIdentifier.dbName() : | |
1009 | "Name doesn't exist"); | |
1010 | ||
1011 | ||
1012 | //"Name doesn't exist"); | |
1013 | ||
1014 | struct stat st; | |
1015 | int st_result = stat(mDefaultDLDbIdentifier.dbName(), &st); | |
1016 | ||
1017 | ||
1018 | if (st_result) | |
1019 | { | |
1020 | secdebug("secpref", "stat(%s) -> %d", mDefaultDLDbIdentifier.dbName(), st_result); | |
1021 | mDefaultDLDbIdentifier = DLDbIdentifier(); // initialize a NULL keychain | |
1022 | secdebug("secpref", "after DLDbIdentifier(), we think the default keychain is %s", static_cast<bool>(mDefaultDLDbIdentifier) ? mDefaultDLDbIdentifier.dbName() : "<NULL>"); | |
1023 | } | |
1024 | } | |
1025 | ||
1026 | mDefaultDLDbIdentifierSet = true; | |
1027 | } | |
1028 | ||
1029 | ||
1030 | return mDefaultDLDbIdentifier; | |
1031 | } | |
1032 | ||
1033 | void | |
1034 | DLDbListCFPref::loginDLDbIdentifier(const DLDbIdentifier &dlDbIdentifier) | |
1035 | { | |
1036 | if (!(loginDLDbIdentifier() == dlDbIdentifier)) | |
1037 | { | |
1038 | mLoginDLDbIdentifier = dlDbIdentifier; | |
1039 | changed(true); | |
1040 | } | |
1041 | } | |
1042 | ||
1043 | const DLDbIdentifier & | |
1044 | DLDbListCFPref::loginDLDbIdentifier() | |
1045 | { | |
1046 | if (!mLoginDLDbIdentifierSet) | |
1047 | { | |
1048 | CFArrayRef loginArray = reinterpret_cast<CFArrayRef>(CFDictionaryGetValue(mPropertyList, kLoginKeychainKey)); | |
1049 | if (loginArray && CFGetTypeID(loginArray) != CFArrayGetTypeID()) | |
1050 | loginArray = NULL; | |
1051 | ||
1052 | if (loginArray && CFArrayGetCount(loginArray) > 0) | |
1053 | { | |
1054 | CFDictionaryRef loginDict = reinterpret_cast<CFDictionaryRef>(CFArrayGetValueAtIndex(loginArray, 0)); | |
1055 | try | |
1056 | { | |
1057 | secdebug("secpref", "Getting login DLDbIdentifier from loginDict"); | |
1058 | mLoginDLDbIdentifier = cfDictionaryRefToDLDbIdentifier(loginDict); | |
1059 | secdebug("secpref", "we think the login keychain is %s", static_cast<bool>(mLoginDLDbIdentifier) ? mLoginDLDbIdentifier.dbName() : "<NULL>"); | |
1060 | } | |
1061 | catch (...) | |
1062 | { | |
1063 | // If loginArray doesn't parse fall back on the default way of getting the login keychain. | |
1064 | loginArray = NULL; | |
1065 | } | |
1066 | } | |
1067 | ||
1068 | if (!loginArray) | |
1069 | { | |
1070 | mLoginDLDbIdentifier = LoginDLDbIdentifier(); | |
1071 | secdebug("secpref", "after LoginDLDbIdentifier(), we think the login keychain is %s", static_cast<bool>(mLoginDLDbIdentifier) ? mLoginDLDbIdentifier.dbName() : "<NULL>"); | |
1072 | } | |
1073 | ||
1074 | mLoginDLDbIdentifierSet = true; | |
1075 | } | |
1076 | ||
1077 | return mLoginDLDbIdentifier; | |
1078 | } |