2 * Copyright (c) 2013-2017 Apple 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@
24 #import <Accounts/Accounts.h>
25 #import <Accounts/ACAccountStore_Private.h>
26 #import <Accounts/ACAccountType_Private.h>
27 #import <AggregateDictionary/ADClient.h>
28 #import <AppSupport/AppSupportUtils.h>
29 #import <AppleAccount/AppleAccount.h>
30 #import <AppleAccount/ACAccountStore+AppleAccount.h>
31 #import <CloudServices/SecureBackup.h>
32 #import <CoreFoundation/CFUserNotification.h>
33 #import <Foundation/Foundation.h>
34 #import <ManagedConfiguration/MCProfileConnection.h>
35 #import <ManagedConfiguration/MCFeatures.h>
36 #import <MobileCoreServices/MobileCoreServices.h>
37 #import <MobileCoreServices/LSApplicationWorkspace.h>
38 #import <MobileGestalt.h>
39 #import <os/transaction_private.h>
40 #import <ProtectedCloudStorage/CloudIdentity.h>
41 #import <Security/SecFrameworkStrings.h>
42 #import <SpringBoardServices/SBSCFUserNotificationKeys.h>
43 #include <dispatch/dispatch.h>
44 #include "keychain/SecureObjectSync/SOSCloudCircle.h"
45 #include "keychain/SecureObjectSync/SOSCloudCircleInternal.h"
46 #include "keychain/SecureObjectSync/SOSPeerInfo.h"
47 #include "keychain/SecureObjectSync/SOSInternal.h"
51 #import "NSArray+map.h"
52 #import "PersistentState.h"
53 #include <xpc/private.h>
55 #import "NSDate+TimeIntervalDescription.h"
56 #include <xpc/activity.h>
57 #include <xpc/private.h>
58 #import "os/activity.h"
60 #include "utilities/SecCFRelease.h"
61 #include "utilities/debugging.h"
62 #include "utilities/SecAKSWrappers.h"
63 #include "utilities/SecCFWrappers.h"
64 #include <utilities/SecXPCError.h>
66 #import "CoreCDP/CDPFollowUpController.h"
67 #import "CoreCDP/CDPFollowUpContext.h"
68 #import <CoreCDP/CDPAccount.h>
70 // As long as we are logging the failure use exit code of zero to make launchd happy
71 #define EXIT_LOGGED_FAILURE(code) exit(0)
73 const char *kLaunchLaterXPCName = "com.apple.security.CircleJoinRequestedTick";
74 CFRunLoopSourceRef currentAlertSource = NULL;
75 CFUserNotificationRef currentAlert = NULL;
76 bool currentAlertIsForApplicants = true;
77 bool currentAlertIsForKickOut = false;
78 NSMutableDictionary *applicants = nil;
79 volatile NSString *debugState = @"main?";
80 dispatch_block_t doOnceInMainBlockChain = NULL;
81 bool _isLocked = true;
82 bool processApplicantsAfterUnlock = false;
83 bool _unlockedSinceBoot = false;
84 bool _hasPostedFollowupAndStillInError = false;
85 bool _isAccountICDP = false;
86 bool _executeProcessEventsOnce = false;
88 NSString *castleKeychainUrl = @"prefs:root=APPLE_ACCOUNT&path=ICLOUD_SERVICE/com.apple.Dataclass.KeychainSync/ADVANCED";
89 NSString *rejoinICDPUrl = @"prefs:root=APPLE_ACCOUNT&aaaction=CDP&command=rejoin";
91 BOOL processRequests(CFErrorRef *error);
93 static BOOL isErrorFromXPC(CFErrorRef error)
95 // Error due to XPC failure does not provide information about the circle.
96 if (error && (CFEqual(sSecXPCErrorDomain, CFErrorGetDomain(error)))) {
97 secnotice("cjr", "XPC error while checking circle status: \"%@\", not processing events", error);
103 static void PSKeychainSyncIsUsingICDP(void)
105 ACAccountStore *accountStore = [[ACAccountStore alloc] init];
106 ACAccount *account = [accountStore aa_primaryAppleAccount];
107 NSString *dsid = account.accountProperties[@"personID"];
108 BOOL isICDPEnabled = NO;
110 isICDPEnabled = [CDPAccount isICDPEnabledForDSID:dsid];
111 NSLog(@"iCDP: PSKeychainSyncIsUsingICDP returning %@", isICDPEnabled ? @"TRUE" : @"FALSE");
113 NSLog(@"iCDP: no primary account");
116 _isAccountICDP = isICDPEnabled;
117 secnotice("cjr", "account is icdp: %d", _isAccountICDP);
120 static void keybagDidLock()
122 secnotice("cjr", "keybagDidLock");
125 static void keybagDidUnlock()
127 secnotice("cjr", "keybagDidUnlock");
129 CFErrorRef error = NULL;
131 if(processApplicantsAfterUnlock){
132 processRequests(&error);
133 processApplicantsAfterUnlock = false;
136 SOSCCStatus circleStatus = SOSCCThisDeviceIsInCircle(&error);
137 BOOL xpcError = isErrorFromXPC(error);
138 if(xpcError && circleStatus == kSOSCCError) {
139 secnotice("cjr", "returning early due to error returned from securityd: %@", error);
142 else if (_isAccountICDP && (circleStatus == kSOSCCError || circleStatus == kSOSCCCircleAbsent || circleStatus == kSOSCCNotInCircle) && _hasPostedFollowupAndStillInError == false) {
143 NSError *localError = nil;
144 CDPFollowUpContext *context = [CDPFollowUpContext contextForStateRepair];
145 CDPFollowUpController *cdpd = [[CDPFollowUpController alloc] init];
146 [cdpd postFollowUpWithContext:context error:&localError ];
147 secnotice("cjr", "account is icdp");
149 secnotice("cjr", "request to CoreCDP to follow up failed: %@", localError);
152 secnotice("cjr", "CoreCDP handling follow up");
153 _hasPostedFollowupAndStillInError = true;
156 else if(_isAccountICDP && circleStatus == kSOSCCInCircle){
157 _hasPostedFollowupAndStillInError = false;
160 secnotice("cjr", "account not icdp");
164 static bool updateIsLocked ()
166 CFErrorRef aksError = NULL;
167 if (!SecAKSGetIsLocked(&_isLocked, &aksError)) {
169 secerror("Got error querying lock state: %@", aksError);
170 CFReleaseSafe(aksError);
174 _unlockedSinceBoot = YES;
178 static void keybagStateChange ()
180 secerror("osactivity initiated");
181 os_activity_initiate("keybagStateChanged", OS_ACTIVITY_FLAG_DEFAULT, ^{
182 BOOL wasLocked = _isLocked;
183 if ( updateIsLocked()) {
184 if (wasLocked == _isLocked)
185 secerror("still %s ignoring", _isLocked ? "locked" : "unlocked");
194 static void doOnceInMain(dispatch_block_t block)
196 if (doOnceInMainBlockChain) {
197 doOnceInMainBlockChain = ^{
198 doOnceInMainBlockChain();
202 doOnceInMainBlockChain = block;
207 static NSString *appleIDAccountName()
209 ACAccountStore *accountStore = [[ACAccountStore alloc] init];
210 ACAccount *primaryAppleAccount = [accountStore aa_primaryAppleAccount];
211 return primaryAppleAccount.username;
215 static CFOptionFlags flagsForAsk(Applicant *applicant)
217 return kCFUserNotificationPlainAlertLevel | CFUserNotificationSecureTextField(0);
221 // NOTE: gives precedence to OnScreen
222 static Applicant *firstApplicantWaitingOrOnScreen()
224 Applicant *waiting = nil;
225 for (Applicant *applicant in [applicants objectEnumerator]) {
226 if (applicant.applicantUIState == ApplicantOnScreen) {
228 } else if (applicant.applicantUIState == ApplicantWaiting) {
237 static NSMutableArray *applicantsInState(ApplicantUIState state)
239 NSMutableArray *results = [NSMutableArray new];
240 for (Applicant *applicant in [applicants objectEnumerator]) {
241 if (applicant.applicantUIState == state) {
242 [results addObject:applicant];
250 BOOL processRequests(CFErrorRef *error) {
251 NSMutableArray *toAccept = [[applicantsInState(ApplicantAccepted) mapWithBlock:^id(id obj) {return (id)[obj rawPeerInfo];}] mutableCopy];
252 NSMutableArray *toReject = [[applicantsInState(ApplicantRejected) mapWithBlock:^id(id obj) {return (id)[obj rawPeerInfo];}] mutableCopy];
255 if ([toAccept count]) {
256 secnotice("cjr", "Process accept: %@", toAccept);
257 ok = ok && SOSCCAcceptApplicants((__bridge CFArrayRef) toAccept, error);
259 secnotice("cjr", "kSOSCCHoldLockForInitialSync");
260 notify_post(kSOSCCHoldLockForInitialSync);
264 if ([toReject count]) {
265 secnotice("cjr", "Process reject: %@", toReject);
266 ok = ok && SOSCCRejectApplicants((__bridge CFArrayRef) toReject, error);
273 static void cancelCurrentAlert(bool stopRunLoop) {
274 if (currentAlertSource) {
275 CFRunLoopRemoveSource(CFRunLoopGetCurrent(), currentAlertSource, kCFRunLoopDefaultMode);
276 CFReleaseNull(currentAlertSource);
279 CFUserNotificationCancel(currentAlert);
280 CFReleaseNull(currentAlert);
283 CFRunLoopStop(CFRunLoopGetCurrent());
285 currentAlertIsForKickOut = currentAlertIsForApplicants = false;
289 static void askAboutAll(bool passwordFailure);
292 static void applicantChoice(CFUserNotificationRef userNotification, CFOptionFlags responseFlags)
294 ApplicantUIState choice;
296 if (kCFUserNotificationAlternateResponse == responseFlags) {
297 choice = ApplicantRejected;
298 } else if (kCFUserNotificationDefaultResponse == responseFlags) {
299 choice = ApplicantAccepted;
301 secnotice("cjr", "Unexpected response %lu", responseFlags);
302 choice = ApplicantRejected;
306 CFErrorRef error = NULL;
307 NSArray *onScreen = applicantsInState(ApplicantOnScreen);
309 [onScreen enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
310 Applicant* applicant = (Applicant *) obj;
311 applicant.applicantUIState = choice;
314 if (choice == ApplicantRejected) {
315 // If this device has ever set up the public key this should work without the password...
316 processed = processRequests(&error);
318 secnotice("cjr", "Didn't need password to process %@", onScreen);
319 cancelCurrentAlert(true);
322 // ...however if the public key gets lost we should "just" fall through to the validate
324 secnotice("cjr", "Couldn't process reject without password (e=%@) for %@ (will try with password next)", error, onScreen);
326 if(CFErrorIsMalfunctioningKeybagError(error)){
327 secnotice("cjr", "system is locked, dismiss the notification");
328 processApplicantsAfterUnlock = true;
332 CFReleaseNull(error);
335 NSString *password = (__bridge NSString *) CFUserNotificationGetResponseValue(userNotification, kCFUserNotificationTextFieldValuesKey, 0);
337 secnotice("cjr", "No password given, retry");
341 const char *passwordUTF8 = [password UTF8String];
342 NSData *passwordBytes = [NSData dataWithBytes:passwordUTF8 length:strlen(passwordUTF8)];
344 // Sometimes securityd crashes between SOSCCRegisterUserCredentials and processRequests
345 // (which results in a process error -- I think this is 13355140), as a workaround we retry
346 // failure a few times before we give up.
347 for (int try = 0; try < 5 && !processed; try++) {
348 if (!SOSCCTryUserCredentials(CFSTR(""), (__bridge CFDataRef) passwordBytes, &error)) {
349 secnotice("cjr", "Try user credentials failed %@", error);
350 if ((error == NULL) ||
351 (CFEqual(kSOSErrorDomain, CFErrorGetDomain(error)) && kSOSErrorWrongPassword == CFErrorGetCode(error))) {
352 secnotice("cjr", "Calling askAboutAll again...");
353 [onScreen enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
354 Applicant *applicant = (Applicant*) obj;
355 applicant.applicantUIState = ApplicantWaiting;
358 CFReleaseNull(error);
361 EXIT_LOGGED_FAILURE(EX_DATAERR);
364 processed = processRequests(&error);
366 secnotice("cjr", "Can't processRequests: %@ for %@", error, onScreen);
368 CFReleaseNull(error);
371 if (processed && firstApplicantWaitingOrOnScreen()) {
372 cancelCurrentAlert(false);
375 cancelCurrentAlert(true);
380 static void passwordFailurePrompt()
382 #pragma clang diagnostic push
383 #pragma clang diagnostic ignored "-Wformat-nonliteral"
384 NSString *pwIncorrect = [NSString stringWithFormat:(NSString *)CFBridgingRelease(SecCopyCKString(SEC_CK_PASSWORD_INCORRECT)), appleIDAccountName()];
385 #pragma clang diagnostic pop
386 NSString *tryAgain = CFBridgingRelease(SecCopyCKString(SEC_CK_TRY_AGAIN));
387 NSDictionary *noteAttributes = @{
388 (id) kCFUserNotificationAlertHeaderKey : pwIncorrect,
389 (id) kCFUserNotificationDefaultButtonTitleKey : tryAgain,
390 (id) kCFUserNotificationAlertTopMostKey : @YES, // get us onto the lock screen
391 (__bridge id) SBUserNotificationDontDismissOnUnlock: @YES,
392 (__bridge id) SBUserNotificationDismissOnLock : @NO,
394 CFOptionFlags flags = kCFUserNotificationPlainAlertLevel;
396 CFUserNotificationRef note = CFUserNotificationCreate(NULL, 0.0, flags, &err, (__bridge CFDictionaryRef) noteAttributes);
399 CFUserNotificationReceiveResponse(note, 0.0, &flags);
405 static NSString *getLocalizedApprovalBody(NSString *deviceType) {
406 CFStringRef applicationReminder = NULL;
408 if ([deviceType isEqualToString:@"iPhone"])
409 applicationReminder = SecCopyCKString(SEC_CK_APPROVAL_BODY_IOS_IPHONE);
410 else if ([deviceType isEqualToString:@"iPod"])
411 applicationReminder = SecCopyCKString(SEC_CK_APPROVAL_BODY_IOS_IPOD);
412 else if ([deviceType isEqualToString:@"iPad"])
413 applicationReminder = SecCopyCKString(SEC_CK_APPROVAL_BODY_IOS_IPAD);
414 else if ([deviceType isEqualToString:@"Mac"])
415 applicationReminder = SecCopyCKString(SEC_CK_APPROVAL_BODY_IOS_MAC);
417 applicationReminder = SecCopyCKString(SEC_CK_APPROVAL_BODY_IOS_GENERIC);
419 return (__bridge_transfer NSString *) applicationReminder;
423 static NSDictionary *createNote(Applicant *applicantToAskAbout)
425 if(!applicantToAskAbout || !applicantToAskAbout.name || !applicantToAskAbout.deviceType)
428 #pragma clang diagnostic push
429 #pragma clang diagnostic ignored "-Wformat-nonliteral"
430 NSString *header = [NSString stringWithFormat: (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_APPROVAL_TITLE), applicantToAskAbout.name];
431 NSString *body = [NSString stringWithFormat: getLocalizedApprovalBody(applicantToAskAbout.deviceType), appleIDAccountName()];
432 #pragma clang diagnostic pop
435 (id) kCFUserNotificationAlertHeaderKey : header,
436 (id) kCFUserNotificationAlertMessageKey : body,
437 (id) kCFUserNotificationDefaultButtonTitleKey : (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_APPROVE),
438 (id) kCFUserNotificationAlternateButtonTitleKey: (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_DECLINE),
439 (id) kCFUserNotificationTextFieldTitlesKey : (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_ICLOUD_PASSWORD),
440 (id) kCFUserNotificationAlertTopMostKey : @YES, // get us onto the lock screen
441 (__bridge_transfer id) SBUserNotificationDontDismissOnUnlock: @YES,
442 (__bridge_transfer id) SBUserNotificationDismissOnLock : @NO,
447 static void askAboutAll(bool passwordFailure)
449 if ([[MCProfileConnection sharedConnection] effectiveBoolValueForSetting: MCFeatureAccountModificationAllowed] == MCRestrictedBoolExplicitNo) {
450 secnotice("cjr", "Account modifications not allowed.");
454 if (passwordFailure) {
455 passwordFailurePrompt();
458 if ((passwordFailure || !currentAlertIsForApplicants) && currentAlert) {
459 if (!currentAlertIsForApplicants) {
460 CFUserNotificationCancel(currentAlert);
462 // after password failure we need to remove the existing alert and supporting objects
463 // because we can't reuse them.
464 CFReleaseNull(currentAlert);
465 CFReleaseNull(currentAlertSource);
467 currentAlertIsForApplicants = true;
469 Applicant *applicantToAskAbout = firstApplicantWaitingOrOnScreen();
470 secnotice("cjr", "Asking about: %@ (of: %@)", applicantToAskAbout, applicants);
472 NSDictionary *noteAttributes = createNote(applicantToAskAbout);
473 if(!noteAttributes) {
474 secnotice("cjr", "NULL data for %@", applicantToAskAbout);
475 cancelCurrentAlert(true);
479 CFOptionFlags flags = flagsForAsk(applicantToAskAbout);
482 SInt32 err = CFUserNotificationUpdate(currentAlert, 0, flags, (__bridge CFDictionaryRef) noteAttributes);
484 secnotice("cjr", "CFUserNotificationUpdate err=%d", (int)err);
485 EXIT_LOGGED_FAILURE(EX_SOFTWARE);
489 currentAlert = CFUserNotificationCreate(NULL, 0.0, flags, &err, (__bridge CFDictionaryRef) noteAttributes);
491 secnotice("cjr", "Can't make notification for %@ err=%x", applicantToAskAbout, (int)err);
492 EXIT_LOGGED_FAILURE(EX_SOFTWARE);
495 currentAlertSource = CFUserNotificationCreateRunLoopSource(NULL, currentAlert, applicantChoice, 0);
496 CFRunLoopAddSource(CFRunLoopGetCurrent(), currentAlertSource, kCFRunLoopDefaultMode);
499 applicantToAskAbout.applicantUIState = ApplicantOnScreen;
503 static void scheduleActivity(int alertInterval)
505 xpc_object_t options = xpc_dictionary_create(NULL, NULL, 0);
506 xpc_dictionary_set_uint64(options, XPC_ACTIVITY_DELAY, alertInterval);
507 xpc_dictionary_set_uint64(options, XPC_ACTIVITY_GRACE_PERIOD, XPC_ACTIVITY_INTERVAL_1_MIN);
508 xpc_dictionary_set_bool(options, XPC_ACTIVITY_REPEATING, false);
509 xpc_dictionary_set_bool(options, XPC_ACTIVITY_ALLOW_BATTERY, true);
510 xpc_dictionary_set_string(options, XPC_ACTIVITY_PRIORITY, XPC_ACTIVITY_PRIORITY_UTILITY);
512 xpc_activity_register(kLaunchLaterXPCName, options, ^(xpc_activity_t activity) {
513 secnotice("cjr", "activity handler fired");
518 static void reminderChoice(CFUserNotificationRef userNotification, CFOptionFlags responseFlags) {
519 if (responseFlags == kCFUserNotificationAlternateResponse || responseFlags == kCFUserNotificationDefaultResponse) {
520 PersistentState *state = [PersistentState loadFromStorage];
521 NSDate *nowish = [NSDate new];
522 state.pendingApplicationReminder = [nowish dateByAddingTimeInterval: state.pendingApplicationReminderAlertInterval];
523 scheduleActivity(state.pendingApplicationReminderAlertInterval);
524 [state writeToStorage];
525 if (responseFlags == kCFUserNotificationAlternateResponse) {
527 BOOL ok = [[LSApplicationWorkspace defaultWorkspace] openSensitiveURL:[NSURL URLWithString:castleKeychainUrl] withOptions:nil];
528 secnotice("cjr", "%s iCSC: opening %@ ok=%d", __FUNCTION__, castleKeychainUrl, ok);
532 cancelCurrentAlert(true);
536 static bool iCloudResetAvailable() {
537 SecureBackup *backupd = [SecureBackup new];
538 NSDictionary *backupdResults;
539 NSError *error = [backupd getAccountInfoWithInfo:nil results:&backupdResults];
540 secnotice("cjr", "SecureBackup e=%@ r=%@", error, backupdResults);
541 return (error == nil && [backupdResults[kSecureBackupIsEnabledKey] isEqualToNumber:@YES]);
545 static NSString *getLocalizedApplicationReminder() {
546 CFStringRef applicationReminder = NULL;
547 switch (MGGetSInt32Answer(kMGQDeviceClassNumber, MGDeviceClassInvalid)) {
548 case MGDeviceClassiPhone:
549 applicationReminder = SecCopyCKString(SEC_CK_REMINDER_BODY_IOS_IPHONE);
551 case MGDeviceClassiPod:
552 applicationReminder = SecCopyCKString(SEC_CK_REMINDER_BODY_IOS_IPOD);
554 case MGDeviceClassiPad:
555 applicationReminder = SecCopyCKString(SEC_CK_REMINDER_BODY_IOS_IPAD);
558 applicationReminder = SecCopyCKString(SEC_CK_REMINDER_BODY_IOS_GENERIC);
561 return (__bridge_transfer NSString *) applicationReminder;
565 static void postApplicationReminderAlert(NSDate *nowish, PersistentState *state, unsigned int alertInterval)
567 NSString *body = getLocalizedApplicationReminder();
568 bool has_iCSC = iCloudResetAvailable();
570 if (CPIsInternalDevice() &&
571 state.defaultPendingApplicationReminderAlertInterval != state.pendingApplicationReminderAlertInterval) {
573 body = [body stringByAppendingFormat: @"〖debug interval %u; wait time %@〗",
574 state.pendingApplicationReminderAlertInterval,
575 [nowish copyDescriptionOfIntervalSince:state.applicationDate]];
579 NSDictionary *pendingAttributes = @{
580 (id) kCFUserNotificationAlertHeaderKey : CFBridgingRelease(SecCopyCKString(SEC_CK_REMINDER_TITLE_IOS)),
581 (id) kCFUserNotificationAlertMessageKey : body,
582 (id) kCFUserNotificationDefaultButtonTitleKey : CFBridgingRelease(SecCopyCKString(SEC_CK_REMINDER_BUTTON_OK)),
583 (id) kCFUserNotificationAlternateButtonTitleKey: has_iCSC ? CFBridgingRelease(SecCopyCKString(SEC_CK_REMINDER_BUTTON_ICSC)) : @"",
584 (id) kCFUserNotificationAlertTopMostKey : @YES,
585 (__bridge id) SBUserNotificationDontDismissOnUnlock : @YES,
586 (__bridge id) SBUserNotificationDismissOnLock : @NO,
589 currentAlert = CFUserNotificationCreate(NULL, 0.0, kCFUserNotificationPlainAlertLevel, &err, (__bridge CFDictionaryRef) pendingAttributes);
592 secnotice("cjr", "Can't make pending notification err=%x", (int)err);
594 currentAlertIsForApplicants = false;
595 currentAlertSource = CFUserNotificationCreateRunLoopSource(NULL, currentAlert, reminderChoice, 0);
596 CFRunLoopAddSource(CFRunLoopGetCurrent(), currentAlertSource, kCFRunLoopDefaultMode);
601 static void kickOutChoice(CFUserNotificationRef userNotification, CFOptionFlags responseFlags) {
602 secnotice("cjr", "kOC %@ %lu", userNotification, responseFlags);
604 //default response: continue -> settings pref pane advanced keychain sync page
605 if (responseFlags == kCFUserNotificationDefaultResponse) {
606 // We need to let things unwind to main for the new state to get saved
608 NSURL *url = [NSURL URLWithString: _isAccountICDP ? rejoinICDPUrl : castleKeychainUrl];
609 BOOL ok = [[LSApplicationWorkspace defaultWorkspace] openSensitiveURL:url withOptions:nil];
610 secnotice("cjr","kickOutChoice account is iCDP: %d", _isAccountICDP);
611 secnotice("cjr", "ok=%d opening %@", ok, url);
614 //alternate response: later -> call CD
615 else if (responseFlags == kCFUserNotificationAlternateResponse) {
616 // We need to let things unwind to main for the new state to get saved
619 CDPFollowUpController *cdpd = [[CDPFollowUpController alloc] init];
620 NSError *localError = nil;
621 CDPFollowUpContext *context = [CDPFollowUpContext contextForStateRepair];
622 [cdpd postFollowUpWithContext:context error:&localError ];
624 secnotice("cjr", "request to CoreCDP to follow up failed: %@", localError);
627 secnotice("cjr", "CoreCDP handling follow up");
628 _hasPostedFollowupAndStillInError = true;
634 cancelCurrentAlert(true);
638 CFStringRef const CJRAggdDepartureReasonKey = CFSTR("com.apple.security.circlejoinrequested.departurereason");
639 CFStringRef const CJRAggdNumCircleDevicesKey = CFSTR("com.apple.security.circlejoinrequested.numcircledevices");
641 static void postKickedOutAlert(enum DepartureReason reason)
643 NSString *header = nil;
644 NSString *message = nil;
646 // <rdar://problem/20358568> iCDP telemetry: ☂ Statistics on circle reset and drop out events
647 ADClientSetValueForScalarKey(CJRAggdDepartureReasonKey, reason);
649 int64_t num_peers = 0;
650 CFArrayRef peerList = SOSCCCopyPeerPeerInfo(NULL);
652 num_peers = CFArrayGetCount(peerList);
653 if (num_peers > 99) {
654 // Round down # peers to 2 significant digits
656 for (factor = 10; num_peers >= 100*factor; factor *= 10) ;
657 num_peers = (num_peers / factor) * factor;
661 ADClientSetValueForScalarKey(CJRAggdNumCircleDevicesKey, num_peers);
663 debugState = @"pKOA A";
664 secnotice("cjr", "DepartureReason %d", reason);
666 case kSOSDiscoveredRetirement:
667 case kSOSLostPrivateKey:
668 case kSOSWithdrewMembership:
669 // Was: SEC_CK_CR_BODY_WITHDREW
670 // "... if you turn off a switch you have some idea why the light is off" - Murf
673 case kSOSNeverAppliedToCircle:
674 // We didn't get kicked out, we were never here. This should only happen if we changed iCloud accounts
675 // (and we had sync on in the previous one, and never had it on in the new one). As this is explicit
676 // user action alot of the "Light switch" argument (above) applies.
679 case kSOSPasswordChanged:
680 case kSOSNeverLeftCircle:
681 case kSOSMembershipRevoked:
682 case kSOSLeftUntrustedCircle:
684 header = (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_PWD_REQUIRED_TITLE);
685 message = (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_PWD_REQUIRED_BODY_IOS);
689 if (CPIsInternalDevice()) {
690 static const char *departureReasonStrings[] = {
691 "kSOSDepartureReasonError",
692 "kSOSNeverLeftCircle",
693 "kSOSWithdrewMembership",
694 "kSOSMembershipRevoked",
695 "kSOSLeftUntrustedCircle",
696 "kSOSNeverAppliedToCircle",
697 "kSOSDiscoveredRetirement",
698 "kSOSLostPrivateKey",
699 "kSOSPasswordChanged",
702 int idx = (kSOSDepartureReasonError <= reason && reason <= kSOSLostPrivateKey) ? reason : (kSOSLostPrivateKey + 1);
703 #pragma clang diagnostic push
704 #pragma clang diagnostic ignored "-Wformat-nonliteral"
705 NSString *reason_str = [NSString stringWithFormat:(__bridge_transfer NSString *) SecCopyCKString(SEC_CK_CR_REASON_INTERNAL),
706 departureReasonStrings[idx]];
707 #pragma clang diagnostic pop
708 message = [message stringByAppendingString: reason_str];
711 NSDictionary *kickedAttributes = @{
712 (id) kCFUserNotificationAlertHeaderKey : header,
713 (id) kCFUserNotificationAlertMessageKey : message,
714 (id) kCFUserNotificationDefaultButtonTitleKey : (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_CONTINUE),
715 (id) kCFUserNotificationAlternateButtonTitleKey: (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_NOT_NOW),
716 (id) kCFUserNotificationAlertTopMostKey : @YES,
717 (__bridge id) SBUserNotificationDismissOnLock : @NO,
718 (__bridge id) SBUserNotificationDontDismissOnUnlock : @YES,
722 if (currentAlertIsForKickOut) {
723 debugState = @"pKOA B";
724 secnotice("cjr", "Updating existing alert %@ with %@", currentAlert, kickedAttributes);
725 CFUserNotificationUpdate(currentAlert, 0, kCFUserNotificationPlainAlertLevel, (__bridge CFDictionaryRef) kickedAttributes);
727 debugState = @"pKOA C";
729 CFUserNotificationRef note = CFUserNotificationCreate(NULL, 0.0, kCFUserNotificationPlainAlertLevel, &err, (__bridge CFDictionaryRef) kickedAttributes);
730 assert((note == NULL) == (err != 0));
732 secnotice("cjr", "Can't make kicked out notification err=%x", (int)err);
735 currentAlertIsForApplicants = false;
736 currentAlertIsForKickOut = true;
739 secnotice("cjr", "New ko alert %@ a=%@", currentAlert, kickedAttributes);
740 currentAlertSource = CFUserNotificationCreateRunLoopSource(NULL, currentAlert, kickOutChoice, 0);
741 CFRunLoopAddSource(CFRunLoopGetCurrent(), currentAlertSource, kCFRunLoopDefaultMode);
742 int backupStateChangeToken;
743 notify_register_dispatch("com.apple.EscrowSecurityAlert.reset", &backupStateChangeToken, dispatch_get_main_queue(), ^(int token) {
744 if (currentAlert == note) {
745 secnotice("cjr", "Backup state might have changed (dS=%@)", debugState);
746 postKickedOutAlert(reason);
748 secnotice("cjr", "Backup state may have changed, but we don't care anymore (dS=%@)", debugState);
752 debugState = @"pKOA D";
754 debugState = @"pKOA E";
755 notify_cancel(backupStateChangeToken);
758 debugState = @"pKOA Z";
761 static bool processEvents()
763 debugState = @"processEvents A";
765 CFErrorRef error = NULL;
766 CFErrorRef departError = NULL;
767 SOSCCStatus circleStatus = SOSCCThisDeviceIsInCircleNonCached(&error);
768 enum DepartureReason departureReason = SOSCCGetLastDepartureReason(&departError);
770 BOOL abortFromError = isErrorFromXPC(error);
771 if(abortFromError && circleStatus == kSOSCCError) {
772 secnotice("cjr", "returning from processEvents due to error returned from securityd: %@", error);
775 if (departureReason == kSOSDepartureReasonError && departError && (CFEqual(sSecXPCErrorDomain, CFErrorGetDomain(departError)))) {
776 secnotice("cjr", "XPC error while checking last departure reason: \"%@\", not processing events", departError);
780 NSDate *nowish = [NSDate date];
781 PersistentState *state = [PersistentState loadFromStorage];
782 secnotice("cjr", "CircleStatus %d -> %d{%d} (s=%p)", state.lastCircleStatus, circleStatus, departureReason, state);
784 // Pending application reminder
785 NSTimeInterval timeUntilApplicationAlert = [state.pendingApplicationReminder timeIntervalSinceDate:nowish];
786 secnotice("cjr", "Time until pendingApplicationReminder (%@) %f", [state.pendingApplicationReminder debugDescription], timeUntilApplicationAlert);
787 if (circleStatus == kSOSCCRequestPending) {
788 if (timeUntilApplicationAlert <= 0) {
789 debugState = @"reminderAlert";
790 postApplicationReminderAlert(nowish, state, state.pendingApplicationReminderAlertInterval);
792 scheduleActivity(ceil(timeUntilApplicationAlert));
796 PSKeychainSyncIsUsingICDP();
798 // Refresh because sometimes we're fixed elsewhere before we get here.
799 CFReleaseNull(error);
800 circleStatus = SOSCCThisDeviceIsInCircleNonCached(&error);
801 abortFromError = isErrorFromXPC(error);
802 if(abortFromError && circleStatus == kSOSCCError) {
803 secnotice("cjr", "returning from processEvents due to error returned from securityd: %@", error);
808 if((circleStatus == kSOSCCError || circleStatus == kSOSCCCircleAbsent || circleStatus == kSOSCCNotInCircle) && _hasPostedFollowupAndStillInError == false) {
809 if(circleStatus == kSOSCCError) {
810 secnotice("cjr", "error from SOSCCThisDeviceIsInCircle: %@", error);
814 You would think we could count on not being iCDP if the account was signed out. Evidently that's wrong.
815 So we'll go based on the artifact that when the account object is reset (like by signing out) the
816 departureReason will be set to kSOSDepartureReasonError. So we won't push to get back into a circle if that's
817 the current reason. I've checked code for other ways we could be out. If we boot and can't load the account
818 we'll end up with kSOSDepartureReasonError. Then too if we end up in kSOSDepartureReasonError and reboot we end up
819 in the same place. Leave it to cdpd to decide whether the user needs to sign in to an account.
821 if(departureReason != kSOSDepartureReasonError) {
822 secnotice("cjr", "iCDP: We need to get back into the circle");
824 NSError *localError = nil;
825 CDPFollowUpController *cdpd = [[CDPFollowUpController alloc] init];
826 CDPFollowUpContext *context = [CDPFollowUpContext contextForStateRepair];
827 [cdpd postFollowUpWithContext:context error:&localError ];
829 secnotice("cjr", "request to CoreCDP to follow up failed: %@", localError);
832 secnotice("cjr", "CoreCDP handling follow up");
833 _hasPostedFollowupAndStillInError = true;
837 secnotice("cjr", "iCDP: We appear to not be associated with an iCloud account");
839 state.lastCircleStatus = circleStatus;
840 _executeProcessEventsOnce = true;
843 else if(circleStatus == kSOSCCInCircle){
844 secnotice("cjr", "follow up should be resolved");
845 _executeProcessEventsOnce = true;
846 _hasPostedFollowupAndStillInError = false;
849 secnotice("cjr", "followup not resolved");
850 _executeProcessEventsOnce = true;
853 } else if(circleStatus == kSOSCCError && state.lastCircleStatus != kSOSCCError && (departureReason == kSOSNeverLeftCircle)) {
854 secnotice("cjr", "SA: error from SOSCCThisDeviceIsInCircle: %@", error);
855 CFIndex errorCode = CFErrorGetCode(error);
856 if(errorCode == kSOSErrorPublicKeyAbsent){
857 secnotice("cjr", "SA: We need the password to re-validate ourselves - it's changed on another device");
858 postKickedOutAlert(kSOSPasswordChanged);
859 state.lastCircleStatus = kSOSCCError;
860 [state writeToStorage];
864 // No longer in circle?
865 if ((state.lastCircleStatus == kSOSCCInCircle && (circleStatus == kSOSCCNotInCircle || circleStatus == kSOSCCCircleAbsent)) ||
866 (state.lastCircleStatus == kSOSCCCircleAbsent && circleStatus == kSOSCCNotInCircle && state.absentCircleWithNoReason) ||
867 state.debugShowLeftReason) {
868 // Used to be in the circle, now we aren't - tell the user why
869 debugState = @"processEvents B";
871 if (state.debugShowLeftReason) {
872 secnotice("cjr", "debugShowLeftReason: %@", state.debugShowLeftReason);
873 departureReason = [state.debugShowLeftReason intValue];
874 state.debugShowLeftReason = nil;
875 CFReleaseNull(departError);
876 [state writeToStorage];
879 if (departureReason != kSOSDepartureReasonError) {
880 state.absentCircleWithNoReason = (circleStatus == kSOSCCCircleAbsent && departureReason == kSOSNeverLeftCircle);
881 secnotice("cjr", "Depature reason %d", departureReason);
883 secnotice("cjr", "posting revocation notification!");
884 postKickedOutAlert(departureReason);
886 else if(_isAccountICDP && _hasPostedFollowupAndStillInError == false){
887 NSError *localError = nil;
888 CDPFollowUpController *cdpd = [[CDPFollowUpController alloc] init];
889 CDPFollowUpContext *context = [CDPFollowUpContext contextForStateRepair];
890 [cdpd postFollowUpWithContext:context error:&localError ];
892 secnotice("cjr", "request to CoreCDP to follow up failed: %@", localError);
895 secnotice("cjr", "CoreCDP handling follow up");
896 _hasPostedFollowupAndStillInError = true;
900 secnotice("cjr", "still waiting for followup to resolve");
902 secnotice("cjr", "pKOA returned (cS %d lCS %d)", circleStatus, state.lastCircleStatus);
904 secnotice("cjr", "Couldn't get last departure reason: %@", departError);
909 // Circle applications: pending request(s) started / completed
910 debugState = @"processEvents C";
911 if (circleStatus != state.lastCircleStatus) {
912 SOSCCStatus lastCircleStatus = state.lastCircleStatus;
913 state.lastCircleStatus = circleStatus;
915 if (lastCircleStatus != kSOSCCRequestPending && circleStatus == kSOSCCRequestPending) {
916 secnotice("cjr", "Pending request started");
917 state.applicationDate = nowish;
918 state.pendingApplicationReminder = [nowish dateByAddingTimeInterval: state.pendingApplicationReminderAlertInterval];
919 scheduleActivity(state.pendingApplicationReminderAlertInterval);
921 if (lastCircleStatus == kSOSCCRequestPending && circleStatus != kSOSCCRequestPending) {
922 secnotice("cjr", "Pending request completed");
923 state.applicationDate = [NSDate distantPast];
924 state.pendingApplicationReminder = [NSDate distantFuture];
927 [state writeToStorage];
930 if (circleStatus != kSOSCCInCircle) {
931 if (circleStatus == kSOSCCRequestPending && currentAlert) {
933 CFUserNotificationRef postedAlert = currentAlert;
935 debugState = @"processEvents D1";
936 notify_register_dispatch(kSOSCCCircleChangedNotification, ¬ifyToken, dispatch_get_main_queue(), ^(int token) {
937 if (postedAlert != currentAlert) {
938 secnotice("cjr", "-- CC after original alert gone (currentAlertIsForApplicants %d, pA %p, cA %p -- %@)",
939 currentAlertIsForApplicants, postedAlert, currentAlert, currentAlert);
940 notify_cancel(token);
942 CFErrorRef localError = NULL;
943 SOSCCStatus newCircleStatus = SOSCCThisDeviceIsInCircle(&localError);
944 BOOL xpcError = isErrorFromXPC(localError);
945 if(xpcError && newCircleStatus == kSOSCCError) {
946 secnotice("cjr", "returning from processEvents due to error returned from securityd: %@", localError);
950 if (newCircleStatus != kSOSCCRequestPending) {
951 if (newCircleStatus == kSOSCCError)
952 secnotice("cjr", "No longer pending (nCS=%d, alert=%@) error: %@", newCircleStatus, currentAlert, localError);
954 secnotice("cjr", "No longer pending (nCS=%d, alert=%@)", newCircleStatus, currentAlert);
955 cancelCurrentAlert(true);
957 secnotice("cjr", "Still pending...");
959 CFReleaseNull(localError);
962 debugState = @"processEvents D2";
963 secnotice("cjr", "NOTE: currentAlertIsForApplicants %d, token %d", currentAlertIsForApplicants, notifyToken);
967 debugState = @"processEvents D4";
968 secnotice("cjr", "SOSCCThisDeviceIsInCircle status %d, not checking applicants", circleStatus);
974 debugState = @"processEvents E";
975 applicants = [NSMutableDictionary new];
976 for (id applicantInfo in (__bridge_transfer NSArray *) SOSCCCopyApplicantPeerInfo(&error)) {
977 Applicant *applicant = [[Applicant alloc] initWithPeerInfo:(__bridge SOSPeerInfoRef) applicantInfo];
978 applicants[applicant.idString] = applicant;
981 // Log error from SOSCCCopyApplicantPeerInfo() above?
982 CFReleaseNull(error);
984 int notify_token = -42;
985 debugState = @"processEvents F";
986 int notify_register_status = notify_register_dispatch(kSOSCCCircleChangedNotification, ¬ify_token, dispatch_get_main_queue(), ^(int token) {
987 secnotice("cjr", "Notified: %s", kSOSCCCircleChangedNotification);
988 CFErrorRef circleStatusError = NULL;
990 bool needsUpdate = false;
991 CFErrorRef copyPeerError = NULL;
992 NSMutableSet *newIds = [NSMutableSet new];
993 for (id applicantInfo in (__bridge_transfer NSArray *) SOSCCCopyApplicantPeerInfo(©PeerError)) {
994 Applicant *newApplicant = [[Applicant alloc] initWithPeerInfo:(__bridge SOSPeerInfoRef) applicantInfo];
995 [newIds addObject:newApplicant.idString];
996 Applicant *existingApplicant = applicants[newApplicant.idString];
997 if (existingApplicant) {
998 switch (existingApplicant.applicantUIState) {
999 case ApplicantWaiting:
1000 applicants[newApplicant.idString] = newApplicant;
1003 case ApplicantOnScreen:
1004 newApplicant.applicantUIState = ApplicantOnScreen;
1005 applicants[newApplicant.idString] = newApplicant;
1009 secnotice("cjr", "Update to %@ >> %@ with pending order, should work out ok though", existingApplicant, newApplicant);
1014 applicants[newApplicant.idString] = newApplicant;
1017 if (copyPeerError) {
1018 secnotice("cjr", "Could not update peer info array: %@", copyPeerError);
1019 CFRelease(copyPeerError);
1023 NSMutableArray *idsToRemoveFromApplicants = [NSMutableArray new];
1024 for (NSString *exisitngId in [applicants keyEnumerator]) {
1025 if (![newIds containsObject:exisitngId]) {
1026 [idsToRemoveFromApplicants addObject:exisitngId];
1030 [applicants removeObjectsForKeys:idsToRemoveFromApplicants];
1032 if (newIds.count == 0) {
1033 secnotice("cjr", "All applicants were handled elsewhere");
1034 cancelCurrentAlert(true);
1036 CFErrorRef circleError = NULL;
1037 SOSCCStatus currentCircleStatus = SOSCCThisDeviceIsInCircle(&circleError);
1038 BOOL xpcError = isErrorFromXPC(circleError);
1039 if(xpcError && currentCircleStatus == kSOSCCError) {
1040 secnotice("cjr", "returning early due to error returned from securityd: %@", circleError);
1043 if (kSOSCCInCircle != currentCircleStatus) {
1044 secnotice("cjr", "Left circle (%d), not handling remaining %lu applicants", currentCircleStatus, (unsigned long)newIds.count);
1045 cancelCurrentAlert(true);
1050 secnotice("cjr", "needsUpdate false, not updating alert");
1052 // Log circleStatusError?
1053 CFReleaseNull(circleStatusError);
1055 secnotice("cjr", "ACC token %d, status %d", notify_token, notify_register_status);
1056 debugState = @"processEvents F2";
1058 if (applicants.count == 0) {
1059 secnotice("cjr", "No applicants");
1061 debugState = @"processEvents F3";
1063 debugState = @"processEvents F4";
1065 debugState = @"processEvents F5";
1070 debugState = @"processEvents F6";
1071 notify_cancel(notify_token);
1072 debugState = @"processEvents DONE";
1078 int main (int argc, const char * argv[]) {
1079 os_transaction_t txion = os_transaction_create("com.apple.security.circle-join-requested");
1083 // NOTE: DISPATCH_QUEUE_PRIORITY_LOW will not actually manage to drain events in a lot of cases (like circleStatus != kSOSCCInCircle)
1084 xpc_set_event_stream_handler("com.apple.notifyd.matching", dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^(xpc_object_t object) {
1085 char *event_description = xpc_copy_description(object);
1086 const char *notificationName = xpc_dictionary_get_string(object, "Notification");
1088 if (notificationName && strcmp(notificationName, kUserKeybagStateChangeNotification)==0) {
1089 secnotice("cjr", "keybag changed!");
1090 keybagStateChange();
1093 secnotice("cjr", "notifyd event: %s\nAlert (%p) %s %s\ndebugState: %@", event_description, currentAlert,
1094 currentAlertIsForApplicants ? "for applicants" : "!applicants",
1095 currentAlertIsForKickOut ? "KO" : "!KO", debugState);
1096 free(event_description);
1099 xpc_activity_register(kLaunchLaterXPCName, XPC_ACTIVITY_CHECK_IN, ^(xpc_activity_t activity) {
1102 int falseInARow = 0;
1103 while (falseInARow < 2 && !_executeProcessEventsOnce) {
1104 if (processEvents()) {
1105 secnotice("cjr", "Processed events!!!");
1110 cancelCurrentAlert(false);
1111 if (doOnceInMainBlockChain) {
1112 doOnceInMainBlockChain();
1113 doOnceInMainBlockChain = NULL;
1118 secnotice("cjr", "Done");
1119 (void) txion; // But we really do want this around, compiler...