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>
65 #import <os/variant_private.h>
67 #import "CoreCDP/CDPFollowUpController.h"
68 #import "CoreCDP/CDPFollowUpContext.h"
69 #import <CoreCDP/CDPAccount.h>
71 // As long as we are logging the failure use exit code of zero to make launchd happy
72 #define EXIT_LOGGED_FAILURE(code) exit(0)
74 const char *kLaunchLaterXPCName = "com.apple.security.CircleJoinRequestedTick";
75 CFRunLoopSourceRef currentAlertSource = NULL;
76 CFUserNotificationRef currentAlert = NULL;
77 bool currentAlertIsForApplicants = true;
78 bool currentAlertIsForKickOut = false;
79 NSMutableDictionary *applicants = nil;
80 volatile NSString *debugState = @"main?";
81 dispatch_block_t doOnceInMainBlockChain = NULL;
82 bool _isLocked = true;
83 bool processApplicantsAfterUnlock = false;
84 bool _unlockedSinceBoot = false;
85 bool _hasPostedFollowupAndStillInError = false;
86 bool _isAccountICDP = false;
87 bool _executeProcessEventsOnce = false;
89 NSString *castleKeychainUrl = @"prefs:root=APPLE_ACCOUNT&path=ICLOUD_SERVICE/com.apple.Dataclass.KeychainSync/ADVANCED";
90 NSString *rejoinICDPUrl = @"prefs:root=APPLE_ACCOUNT&aaaction=CDP&command=rejoin";
92 BOOL processRequests(CFErrorRef *error);
94 static BOOL isErrorFromXPC(CFErrorRef error)
96 // Error due to XPC failure does not provide information about the circle.
97 if (error && (CFEqual(sSecXPCErrorDomain, CFErrorGetDomain(error)))) {
98 secnotice("cjr", "XPC error while checking circle status: \"%@\", not processing events", error);
104 static void PSKeychainSyncIsUsingICDP(void)
106 ACAccountStore *accountStore = [[ACAccountStore alloc] init];
107 ACAccount *account = [accountStore aa_primaryAppleAccount];
108 NSString *dsid = account.accountProperties[@"personID"];
109 BOOL isICDPEnabled = NO;
111 isICDPEnabled = [CDPAccount isICDPEnabledForDSID:dsid];
112 NSLog(@"iCDP: PSKeychainSyncIsUsingICDP returning %@", isICDPEnabled ? @"TRUE" : @"FALSE");
114 NSLog(@"iCDP: no primary account");
117 _isAccountICDP = isICDPEnabled;
118 secnotice("cjr", "account is icdp: %d", _isAccountICDP);
121 static void keybagDidLock()
123 secnotice("cjr", "keybagDidLock");
126 static void keybagDidUnlock()
128 secnotice("cjr", "keybagDidUnlock");
130 CFErrorRef error = NULL;
132 if(processApplicantsAfterUnlock){
133 processRequests(&error);
134 processApplicantsAfterUnlock = false;
137 SOSCCStatus circleStatus = SOSCCThisDeviceIsInCircle(&error);
138 BOOL xpcError = isErrorFromXPC(error);
139 if(xpcError && circleStatus == kSOSCCError) {
140 secnotice("cjr", "returning early due to error returned from securityd: %@", error);
143 else if (_isAccountICDP && (circleStatus == kSOSCCError || circleStatus == kSOSCCCircleAbsent || circleStatus == kSOSCCNotInCircle) && _hasPostedFollowupAndStillInError == false) {
144 NSError *localError = nil;
145 CDPFollowUpContext *context = [CDPFollowUpContext contextForStateRepair];
146 CDPFollowUpController *cdpd = [[CDPFollowUpController alloc] init];
147 [cdpd postFollowUpWithContext:context error:&localError ];
148 secnotice("cjr", "account is icdp");
150 secnotice("cjr", "request to CoreCDP to follow up failed: %@", localError);
153 secnotice("cjr", "CoreCDP handling follow up");
154 _hasPostedFollowupAndStillInError = true;
157 else if(_isAccountICDP && circleStatus == kSOSCCInCircle){
158 _hasPostedFollowupAndStillInError = false;
161 secnotice("cjr", "account not icdp");
165 static bool updateIsLocked ()
167 CFErrorRef aksError = NULL;
168 if (!SecAKSGetIsLocked(&_isLocked, &aksError)) {
170 secerror("Got error querying lock state: %@", aksError);
171 CFReleaseSafe(aksError);
175 _unlockedSinceBoot = YES;
179 static void keybagStateChange ()
181 secerror("osactivity initiated");
182 os_activity_initiate("keybagStateChanged", OS_ACTIVITY_FLAG_DEFAULT, ^{
183 BOOL wasLocked = _isLocked;
184 if ( updateIsLocked()) {
185 if (wasLocked == _isLocked)
186 secerror("still %s ignoring", _isLocked ? "locked" : "unlocked");
195 static void doOnceInMain(dispatch_block_t block)
197 if (doOnceInMainBlockChain) {
198 doOnceInMainBlockChain = ^{
199 doOnceInMainBlockChain();
203 doOnceInMainBlockChain = block;
208 static NSString *appleIDAccountName()
210 ACAccountStore *accountStore = [[ACAccountStore alloc] init];
211 ACAccount *primaryAppleAccount = [accountStore aa_primaryAppleAccount];
212 return primaryAppleAccount.username;
216 static CFOptionFlags flagsForAsk(Applicant *applicant)
218 return kCFUserNotificationPlainAlertLevel | CFUserNotificationSecureTextField(0);
222 // NOTE: gives precedence to OnScreen
223 static Applicant *firstApplicantWaitingOrOnScreen()
225 Applicant *waiting = nil;
226 for (Applicant *applicant in [applicants objectEnumerator]) {
227 if (applicant.applicantUIState == ApplicantOnScreen) {
229 } else if (applicant.applicantUIState == ApplicantWaiting) {
238 static NSMutableArray *applicantsInState(ApplicantUIState state)
240 NSMutableArray *results = [NSMutableArray new];
241 for (Applicant *applicant in [applicants objectEnumerator]) {
242 if (applicant.applicantUIState == state) {
243 [results addObject:applicant];
251 BOOL processRequests(CFErrorRef *error) {
252 NSMutableArray *toAccept = [[applicantsInState(ApplicantAccepted) mapWithBlock:^id(id obj) {return (id)[obj rawPeerInfo];}] mutableCopy];
253 NSMutableArray *toReject = [[applicantsInState(ApplicantRejected) mapWithBlock:^id(id obj) {return (id)[obj rawPeerInfo];}] mutableCopy];
256 if ([toAccept count]) {
257 secnotice("cjr", "Process accept: %@", toAccept);
258 ok = ok && SOSCCAcceptApplicants((__bridge CFArrayRef) toAccept, error);
260 secnotice("cjr", "kSOSCCHoldLockForInitialSync");
261 notify_post(kSOSCCHoldLockForInitialSync);
265 if ([toReject count]) {
266 secnotice("cjr", "Process reject: %@", toReject);
267 ok = ok && SOSCCRejectApplicants((__bridge CFArrayRef) toReject, error);
274 static void cancelCurrentAlert(bool stopRunLoop) {
275 if (currentAlertSource) {
276 CFRunLoopRemoveSource(CFRunLoopGetCurrent(), currentAlertSource, kCFRunLoopDefaultMode);
277 CFReleaseNull(currentAlertSource);
280 CFUserNotificationCancel(currentAlert);
281 CFReleaseNull(currentAlert);
284 CFRunLoopStop(CFRunLoopGetCurrent());
286 currentAlertIsForKickOut = currentAlertIsForApplicants = false;
290 static void askAboutAll(bool passwordFailure);
293 static void applicantChoice(CFUserNotificationRef userNotification, CFOptionFlags responseFlags)
295 ApplicantUIState choice;
297 if (kCFUserNotificationAlternateResponse == responseFlags) {
298 choice = ApplicantRejected;
299 } else if (kCFUserNotificationDefaultResponse == responseFlags) {
300 choice = ApplicantAccepted;
302 secnotice("cjr", "Unexpected response %lu", responseFlags);
303 choice = ApplicantRejected;
307 CFErrorRef error = NULL;
308 NSArray *onScreen = applicantsInState(ApplicantOnScreen);
310 [onScreen enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
311 Applicant* applicant = (Applicant *) obj;
312 applicant.applicantUIState = choice;
315 if (choice == ApplicantRejected) {
316 // If this device has ever set up the public key this should work without the password...
317 processed = processRequests(&error);
319 secnotice("cjr", "Didn't need password to process %@", onScreen);
320 cancelCurrentAlert(true);
323 // ...however if the public key gets lost we should "just" fall through to the validate
325 secnotice("cjr", "Couldn't process reject without password (e=%@) for %@ (will try with password next)", error, onScreen);
327 if(CFErrorIsMalfunctioningKeybagError(error)){
328 secnotice("cjr", "system is locked, dismiss the notification");
329 processApplicantsAfterUnlock = true;
333 CFReleaseNull(error);
336 NSString *password = (__bridge NSString *) CFUserNotificationGetResponseValue(userNotification, kCFUserNotificationTextFieldValuesKey, 0);
338 secnotice("cjr", "No password given, retry");
342 const char *passwordUTF8 = [password UTF8String];
343 NSData *passwordBytes = [NSData dataWithBytes:passwordUTF8 length:strlen(passwordUTF8)];
345 // Sometimes securityd crashes between SOSCCRegisterUserCredentials and processRequests
346 // (which results in a process error -- I think this is 13355140), as a workaround we retry
347 // failure a few times before we give up.
348 for (int try = 0; try < 5 && !processed; try++) {
349 if (!SOSCCTryUserCredentials(CFSTR(""), (__bridge CFDataRef) passwordBytes, &error)) {
350 secnotice("cjr", "Try user credentials failed %@", error);
351 if ((error == NULL) ||
352 (CFEqual(kSOSErrorDomain, CFErrorGetDomain(error)) && kSOSErrorWrongPassword == CFErrorGetCode(error))) {
353 secnotice("cjr", "Calling askAboutAll again...");
354 [onScreen enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
355 Applicant *applicant = (Applicant*) obj;
356 applicant.applicantUIState = ApplicantWaiting;
359 CFReleaseNull(error);
362 EXIT_LOGGED_FAILURE(EX_DATAERR);
365 processed = processRequests(&error);
367 secnotice("cjr", "Can't processRequests: %@ for %@", error, onScreen);
369 CFReleaseNull(error);
372 if (processed && firstApplicantWaitingOrOnScreen()) {
373 cancelCurrentAlert(false);
376 cancelCurrentAlert(true);
381 static void passwordFailurePrompt()
383 #pragma clang diagnostic push
384 #pragma clang diagnostic ignored "-Wformat-nonliteral"
385 NSString *pwIncorrect = [NSString stringWithFormat:(NSString *)CFBridgingRelease(SecCopyCKString(SEC_CK_PASSWORD_INCORRECT)), appleIDAccountName()];
386 #pragma clang diagnostic pop
387 NSString *tryAgain = CFBridgingRelease(SecCopyCKString(SEC_CK_TRY_AGAIN));
388 NSDictionary *noteAttributes = @{
389 (id) kCFUserNotificationAlertHeaderKey : pwIncorrect,
390 (id) kCFUserNotificationDefaultButtonTitleKey : tryAgain,
391 (id) kCFUserNotificationAlertTopMostKey : @YES, // get us onto the lock screen
392 (__bridge id) SBUserNotificationDontDismissOnUnlock: @YES,
393 (__bridge id) SBUserNotificationDismissOnLock : @NO,
395 CFOptionFlags flags = kCFUserNotificationPlainAlertLevel;
397 CFUserNotificationRef note = CFUserNotificationCreate(NULL, 0.0, flags, &err, (__bridge CFDictionaryRef) noteAttributes);
400 CFUserNotificationReceiveResponse(note, 0.0, &flags);
406 static NSString *getLocalizedApprovalBody(NSString *deviceType) {
407 CFStringRef applicationReminder = NULL;
409 if ([deviceType isEqualToString:@"iPhone"])
410 applicationReminder = SecCopyCKString(SEC_CK_APPROVAL_BODY_IOS_IPHONE);
411 else if ([deviceType isEqualToString:@"iPod"])
412 applicationReminder = SecCopyCKString(SEC_CK_APPROVAL_BODY_IOS_IPOD);
413 else if ([deviceType isEqualToString:@"iPad"])
414 applicationReminder = SecCopyCKString(SEC_CK_APPROVAL_BODY_IOS_IPAD);
415 else if ([deviceType isEqualToString:@"Mac"])
416 applicationReminder = SecCopyCKString(SEC_CK_APPROVAL_BODY_IOS_MAC);
418 applicationReminder = SecCopyCKString(SEC_CK_APPROVAL_BODY_IOS_GENERIC);
420 return (__bridge_transfer NSString *) applicationReminder;
424 static NSDictionary *createNote(Applicant *applicantToAskAbout)
426 if(!applicantToAskAbout || !applicantToAskAbout.name || !applicantToAskAbout.deviceType)
429 #pragma clang diagnostic push
430 #pragma clang diagnostic ignored "-Wformat-nonliteral"
431 NSString *header = [NSString stringWithFormat: (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_APPROVAL_TITLE), applicantToAskAbout.name];
432 NSString *body = [NSString stringWithFormat: getLocalizedApprovalBody(applicantToAskAbout.deviceType), appleIDAccountName()];
433 #pragma clang diagnostic pop
436 (id) kCFUserNotificationAlertHeaderKey : header,
437 (id) kCFUserNotificationAlertMessageKey : body,
438 (id) kCFUserNotificationDefaultButtonTitleKey : (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_APPROVE),
439 (id) kCFUserNotificationAlternateButtonTitleKey: (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_DECLINE),
440 (id) kCFUserNotificationTextFieldTitlesKey : (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_ICLOUD_PASSWORD),
441 (id) kCFUserNotificationAlertTopMostKey : @YES, // get us onto the lock screen
442 (__bridge_transfer id) SBUserNotificationDontDismissOnUnlock: @YES,
443 (__bridge_transfer id) SBUserNotificationDismissOnLock : @NO,
448 static void askAboutAll(bool passwordFailure)
450 if ([[MCProfileConnection sharedConnection] effectiveBoolValueForSetting: MCFeatureAccountModificationAllowed] == MCRestrictedBoolExplicitNo) {
451 secnotice("cjr", "Account modifications not allowed.");
455 if (passwordFailure) {
456 passwordFailurePrompt();
459 if ((passwordFailure || !currentAlertIsForApplicants) && currentAlert) {
460 if (!currentAlertIsForApplicants) {
461 CFUserNotificationCancel(currentAlert);
463 // after password failure we need to remove the existing alert and supporting objects
464 // because we can't reuse them.
465 CFReleaseNull(currentAlert);
466 CFReleaseNull(currentAlertSource);
468 currentAlertIsForApplicants = true;
470 Applicant *applicantToAskAbout = firstApplicantWaitingOrOnScreen();
471 secnotice("cjr", "Asking about: %@ (of: %@)", applicantToAskAbout, applicants);
473 NSDictionary *noteAttributes = createNote(applicantToAskAbout);
474 if(!noteAttributes) {
475 secnotice("cjr", "NULL data for %@", applicantToAskAbout);
476 cancelCurrentAlert(true);
480 CFOptionFlags flags = flagsForAsk(applicantToAskAbout);
483 SInt32 err = CFUserNotificationUpdate(currentAlert, 0, flags, (__bridge CFDictionaryRef) noteAttributes);
485 secnotice("cjr", "CFUserNotificationUpdate err=%d", (int)err);
486 EXIT_LOGGED_FAILURE(EX_SOFTWARE);
490 currentAlert = CFUserNotificationCreate(NULL, 0.0, flags, &err, (__bridge CFDictionaryRef) noteAttributes);
492 secnotice("cjr", "Can't make notification for %@ err=%x", applicantToAskAbout, (int)err);
493 EXIT_LOGGED_FAILURE(EX_SOFTWARE);
496 currentAlertSource = CFUserNotificationCreateRunLoopSource(NULL, currentAlert, applicantChoice, 0);
497 CFRunLoopAddSource(CFRunLoopGetCurrent(), currentAlertSource, kCFRunLoopDefaultMode);
500 applicantToAskAbout.applicantUIState = ApplicantOnScreen;
504 static void scheduleActivity(int alertInterval)
506 xpc_object_t options = xpc_dictionary_create(NULL, NULL, 0);
507 xpc_dictionary_set_uint64(options, XPC_ACTIVITY_DELAY, alertInterval);
508 xpc_dictionary_set_uint64(options, XPC_ACTIVITY_GRACE_PERIOD, XPC_ACTIVITY_INTERVAL_1_MIN);
509 xpc_dictionary_set_bool(options, XPC_ACTIVITY_REPEATING, false);
510 xpc_dictionary_set_bool(options, XPC_ACTIVITY_ALLOW_BATTERY, true);
511 xpc_dictionary_set_string(options, XPC_ACTIVITY_PRIORITY, XPC_ACTIVITY_PRIORITY_UTILITY);
513 xpc_activity_register(kLaunchLaterXPCName, options, ^(xpc_activity_t activity) {
514 secnotice("cjr", "activity handler fired");
519 static void reminderChoice(CFUserNotificationRef userNotification, CFOptionFlags responseFlags) {
520 if (responseFlags == kCFUserNotificationAlternateResponse || responseFlags == kCFUserNotificationDefaultResponse) {
521 PersistentState *state = [PersistentState loadFromStorage];
522 NSDate *nowish = [NSDate new];
523 state.pendingApplicationReminder = [nowish dateByAddingTimeInterval: state.pendingApplicationReminderAlertInterval];
524 scheduleActivity(state.pendingApplicationReminderAlertInterval);
525 [state writeToStorage];
526 if (responseFlags == kCFUserNotificationAlternateResponse) {
528 BOOL ok = [[LSApplicationWorkspace defaultWorkspace] openSensitiveURL:[NSURL URLWithString:castleKeychainUrl] withOptions:nil];
529 secnotice("cjr", "%s iCSC: opening %@ ok=%d", __FUNCTION__, castleKeychainUrl, ok);
533 cancelCurrentAlert(true);
537 static bool iCloudResetAvailable() {
538 SecureBackup *backupd = [SecureBackup new];
539 NSDictionary *backupdResults;
540 NSError *error = [backupd getAccountInfoWithInfo:nil results:&backupdResults];
541 secnotice("cjr", "SecureBackup e=%@ r=%@", error, backupdResults);
542 return (error == nil && [backupdResults[kSecureBackupIsEnabledKey] isEqualToNumber:@YES]);
546 static NSString *getLocalizedApplicationReminder() {
547 CFStringRef applicationReminder = NULL;
548 switch (MGGetSInt32Answer(kMGQDeviceClassNumber, MGDeviceClassInvalid)) {
549 case MGDeviceClassiPhone:
550 applicationReminder = SecCopyCKString(SEC_CK_REMINDER_BODY_IOS_IPHONE);
552 case MGDeviceClassiPod:
553 applicationReminder = SecCopyCKString(SEC_CK_REMINDER_BODY_IOS_IPOD);
555 case MGDeviceClassiPad:
556 applicationReminder = SecCopyCKString(SEC_CK_REMINDER_BODY_IOS_IPAD);
559 applicationReminder = SecCopyCKString(SEC_CK_REMINDER_BODY_IOS_GENERIC);
562 return (__bridge_transfer NSString *) applicationReminder;
565 static bool isSOSInternalDevice(void) {
566 static dispatch_once_t onceToken;
567 static BOOL internal = NO;
568 dispatch_once(&onceToken, ^{
569 internal = os_variant_has_internal_diagnostics("com.apple.security");
574 static void postApplicationReminderAlert(NSDate *nowish, PersistentState *state, unsigned int alertInterval)
576 NSString *body = getLocalizedApplicationReminder();
577 bool has_iCSC = iCloudResetAvailable();
579 if (isSOSInternalDevice() &&
580 state.defaultPendingApplicationReminderAlertInterval != state.pendingApplicationReminderAlertInterval) {
582 body = [body stringByAppendingFormat: @"〖debug interval %u; wait time %@〗",
583 state.pendingApplicationReminderAlertInterval,
584 [nowish copyDescriptionOfIntervalSince:state.applicationDate]];
588 NSDictionary *pendingAttributes = @{
589 (id) kCFUserNotificationAlertHeaderKey : CFBridgingRelease(SecCopyCKString(SEC_CK_REMINDER_TITLE_IOS)),
590 (id) kCFUserNotificationAlertMessageKey : body,
591 (id) kCFUserNotificationDefaultButtonTitleKey : CFBridgingRelease(SecCopyCKString(SEC_CK_REMINDER_BUTTON_OK)),
592 (id) kCFUserNotificationAlternateButtonTitleKey: has_iCSC ? CFBridgingRelease(SecCopyCKString(SEC_CK_REMINDER_BUTTON_ICSC)) : @"",
593 (id) kCFUserNotificationAlertTopMostKey : @YES,
594 (__bridge id) SBUserNotificationDontDismissOnUnlock : @YES,
595 (__bridge id) SBUserNotificationDismissOnLock : @NO,
598 currentAlert = CFUserNotificationCreate(NULL, 0.0, kCFUserNotificationPlainAlertLevel, &err, (__bridge CFDictionaryRef) pendingAttributes);
601 secnotice("cjr", "Can't make pending notification err=%x", (int)err);
603 currentAlertIsForApplicants = false;
604 currentAlertSource = CFUserNotificationCreateRunLoopSource(NULL, currentAlert, reminderChoice, 0);
605 CFRunLoopAddSource(CFRunLoopGetCurrent(), currentAlertSource, kCFRunLoopDefaultMode);
610 static void kickOutChoice(CFUserNotificationRef userNotification, CFOptionFlags responseFlags) {
611 secnotice("cjr", "kOC %@ %lu", userNotification, responseFlags);
613 //default response: continue -> settings pref pane advanced keychain sync page
614 if (responseFlags == kCFUserNotificationDefaultResponse) {
615 // We need to let things unwind to main for the new state to get saved
617 NSURL *url = [NSURL URLWithString: _isAccountICDP ? rejoinICDPUrl : castleKeychainUrl];
618 BOOL ok = [[LSApplicationWorkspace defaultWorkspace] openSensitiveURL:url withOptions:nil];
619 secnotice("cjr","kickOutChoice account is iCDP: %d", _isAccountICDP);
620 secnotice("cjr", "ok=%d opening %@", ok, url);
623 //alternate response: later -> call CD
624 else if (responseFlags == kCFUserNotificationAlternateResponse) {
625 // We need to let things unwind to main for the new state to get saved
628 CDPFollowUpController *cdpd = [[CDPFollowUpController alloc] init];
629 NSError *localError = nil;
630 CDPFollowUpContext *context = [CDPFollowUpContext contextForStateRepair];
631 [cdpd postFollowUpWithContext:context error:&localError ];
633 secnotice("cjr", "request to CoreCDP to follow up failed: %@", localError);
636 secnotice("cjr", "CoreCDP handling follow up");
637 _hasPostedFollowupAndStillInError = true;
643 cancelCurrentAlert(true);
647 CFStringRef const CJRAggdDepartureReasonKey = CFSTR("com.apple.security.circlejoinrequested.departurereason");
648 CFStringRef const CJRAggdNumCircleDevicesKey = CFSTR("com.apple.security.circlejoinrequested.numcircledevices");
650 static void postKickedOutAlert(enum DepartureReason reason)
652 NSString *header = nil;
653 NSString *message = nil;
655 // <rdar://problem/20358568> iCDP telemetry: ☂ Statistics on circle reset and drop out events
656 ADClientSetValueForScalarKey(CJRAggdDepartureReasonKey, reason);
658 int64_t num_peers = 0;
659 CFArrayRef peerList = SOSCCCopyPeerPeerInfo(NULL);
661 num_peers = CFArrayGetCount(peerList);
662 if (num_peers > 99) {
663 // Round down # peers to 2 significant digits
665 for (factor = 10; num_peers >= 100*factor; factor *= 10) ;
666 num_peers = (num_peers / factor) * factor;
670 ADClientSetValueForScalarKey(CJRAggdNumCircleDevicesKey, num_peers);
672 debugState = @"pKOA A";
673 secnotice("cjr", "DepartureReason %d", reason);
675 case kSOSDiscoveredRetirement:
676 case kSOSLostPrivateKey:
677 case kSOSWithdrewMembership:
678 // Was: SEC_CK_CR_BODY_WITHDREW
679 // "... if you turn off a switch you have some idea why the light is off" - Murf
682 case kSOSNeverAppliedToCircle:
683 // We didn't get kicked out, we were never here. This should only happen if we changed iCloud accounts
684 // (and we had sync on in the previous one, and never had it on in the new one). As this is explicit
685 // user action alot of the "Light switch" argument (above) applies.
688 case kSOSPasswordChanged:
689 case kSOSNeverLeftCircle:
690 case kSOSMembershipRevoked:
691 case kSOSLeftUntrustedCircle:
693 header = (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_PWD_REQUIRED_TITLE);
694 message = (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_PWD_REQUIRED_BODY_IOS);
698 if (isSOSInternalDevice()) {
699 static const char *departureReasonStrings[] = {
700 "kSOSDepartureReasonError",
701 "kSOSNeverLeftCircle",
702 "kSOSWithdrewMembership",
703 "kSOSMembershipRevoked",
704 "kSOSLeftUntrustedCircle",
705 "kSOSNeverAppliedToCircle",
706 "kSOSDiscoveredRetirement",
707 "kSOSLostPrivateKey",
708 "kSOSPasswordChanged",
711 int idx = (kSOSDepartureReasonError <= reason && reason <= kSOSLostPrivateKey) ? reason : (kSOSLostPrivateKey + 1);
712 #pragma clang diagnostic push
713 #pragma clang diagnostic ignored "-Wformat-nonliteral"
714 NSString *reason_str = [NSString stringWithFormat:(__bridge_transfer NSString *) SecCopyCKString(SEC_CK_CR_REASON_INTERNAL),
715 departureReasonStrings[idx]];
716 #pragma clang diagnostic pop
717 message = [message stringByAppendingString: reason_str];
720 NSDictionary *kickedAttributes = @{
721 (id) kCFUserNotificationAlertHeaderKey : header,
722 (id) kCFUserNotificationAlertMessageKey : message,
723 (id) kCFUserNotificationDefaultButtonTitleKey : (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_CONTINUE),
724 (id) kCFUserNotificationAlternateButtonTitleKey: (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_NOT_NOW),
725 (id) kCFUserNotificationAlertTopMostKey : @YES,
726 (__bridge id) SBUserNotificationDismissOnLock : @NO,
727 (__bridge id) SBUserNotificationDontDismissOnUnlock : @YES,
731 if (currentAlertIsForKickOut) {
732 debugState = @"pKOA B";
733 secnotice("cjr", "Updating existing alert %@ with %@", currentAlert, kickedAttributes);
734 CFUserNotificationUpdate(currentAlert, 0, kCFUserNotificationPlainAlertLevel, (__bridge CFDictionaryRef) kickedAttributes);
736 debugState = @"pKOA C";
738 CFUserNotificationRef note = CFUserNotificationCreate(NULL, 0.0, kCFUserNotificationPlainAlertLevel, &err, (__bridge CFDictionaryRef) kickedAttributes);
739 assert((note == NULL) == (err != 0));
741 secnotice("cjr", "Can't make kicked out notification err=%x", (int)err);
744 currentAlertIsForApplicants = false;
745 currentAlertIsForKickOut = true;
748 secnotice("cjr", "New ko alert %@ a=%@", currentAlert, kickedAttributes);
749 currentAlertSource = CFUserNotificationCreateRunLoopSource(NULL, currentAlert, kickOutChoice, 0);
750 CFRunLoopAddSource(CFRunLoopGetCurrent(), currentAlertSource, kCFRunLoopDefaultMode);
751 int backupStateChangeToken;
752 notify_register_dispatch("com.apple.EscrowSecurityAlert.reset", &backupStateChangeToken, dispatch_get_main_queue(), ^(int token) {
753 if (currentAlert == note) {
754 secnotice("cjr", "Backup state might have changed (dS=%@)", debugState);
755 postKickedOutAlert(reason);
757 secnotice("cjr", "Backup state may have changed, but we don't care anymore (dS=%@)", debugState);
761 debugState = @"pKOA D";
763 debugState = @"pKOA E";
764 notify_cancel(backupStateChangeToken);
767 debugState = @"pKOA Z";
770 static void askForCDPFollowup() {
772 NSError *localError = nil;
773 CDPFollowUpController *cdpd = [[CDPFollowUpController alloc] init];
774 CDPFollowUpContext *context = [CDPFollowUpContext contextForStateRepair];
775 [cdpd postFollowUpWithContext:context error:&localError ];
777 secnotice("cjr", "request to CoreCDP to follow up failed: %@", localError);
780 secnotice("cjr", "CoreCDP handling follow up");
781 _hasPostedFollowupAndStillInError = true;
786 static bool processEvents()
788 debugState = @"processEvents A";
790 CFErrorRef error = NULL;
791 CFErrorRef departError = NULL;
792 SOSCCStatus circleStatus = SOSCCThisDeviceIsInCircleNonCached(&error);
793 enum DepartureReason departureReason = SOSCCGetLastDepartureReason(&departError);
795 BOOL abortFromError = isErrorFromXPC(error);
796 if(abortFromError && circleStatus == kSOSCCError) {
797 secnotice("cjr", "returning from processEvents due to error returned from securityd: %@", error);
800 if (departureReason == kSOSDepartureReasonError && departError && (CFEqual(sSecXPCErrorDomain, CFErrorGetDomain(departError)))) {
801 secnotice("cjr", "XPC error while checking last departure reason: \"%@\", not processing events", departError);
805 NSDate *nowish = [NSDate date];
806 PersistentState *state = [PersistentState loadFromStorage];
807 secnotice("cjr", "CircleStatus %d -> %d{%d} (s=%p)", state.lastCircleStatus, circleStatus, departureReason, state);
809 // Pending application reminder
810 NSTimeInterval timeUntilApplicationAlert = [state.pendingApplicationReminder timeIntervalSinceDate:nowish];
811 secnotice("cjr", "Time until pendingApplicationReminder (%@) %f", [state.pendingApplicationReminder debugDescription], timeUntilApplicationAlert);
812 if (circleStatus == kSOSCCRequestPending) {
813 if (timeUntilApplicationAlert <= 0) {
814 debugState = @"reminderAlert";
815 postApplicationReminderAlert(nowish, state, state.pendingApplicationReminderAlertInterval);
817 scheduleActivity(ceil(timeUntilApplicationAlert));
821 PSKeychainSyncIsUsingICDP();
823 // Refresh because sometimes we're fixed elsewhere before we get here.
824 CFReleaseNull(error);
825 circleStatus = SOSCCThisDeviceIsInCircleNonCached(&error);
826 abortFromError = isErrorFromXPC(error);
827 if(abortFromError && circleStatus == kSOSCCError) {
828 secnotice("cjr", "returning from processEvents due to error returned from securityd: %@", error);
833 state.lastCircleStatus = circleStatus;
834 [state writeToStorage];
835 if(_hasPostedFollowupAndStillInError == true) {
836 secnotice("cjr", "followup not resolved");
837 _executeProcessEventsOnce = true;
841 switch(circleStatus) {
843 secnotice("cjr", "follow up should be resolved");
844 _executeProcessEventsOnce = true;
845 _hasPostedFollowupAndStillInError = false;
848 secnotice("cjr", "error from SOSCCThisDeviceIsInCircle: %@", error);
850 _executeProcessEventsOnce = true;
852 case kSOSCCCircleAbsent:
853 case kSOSCCNotInCircle:
855 You would think we could count on not being iCDP if the account was signed out. Evidently that's wrong.
856 So we'll go based on the artifact that when the account object is reset (like by signing out) the
857 departureReason will be set to kSOSDepartureReasonError. So we won't push to get back into a circle if that's
858 the current reason. I've checked code for other ways we could be out. If we boot and can't load the account
859 we'll end up with kSOSDepartureReasonError. Then too if we end up in kSOSDepartureReasonError and reboot we end up
860 in the same place. Leave it to cdpd to decide whether the user needs to sign in to an account.
862 if(departureReason != kSOSDepartureReasonError) {
863 secnotice("cjr", "iCDP: We need to get back into the circle");
866 secnotice("cjr", "iCDP: We appear to not be associated with an iCloud account");
868 _executeProcessEventsOnce = true;
870 case kSOSCCRequestPending:
873 secnotice("cjr", "Unknown circle status %d", circleStatus);
876 } else if(circleStatus == kSOSCCError && state.lastCircleStatus != kSOSCCError && (departureReason == kSOSNeverLeftCircle)) {
877 secnotice("cjr", "SA: error from SOSCCThisDeviceIsInCircle: %@", error);
878 CFIndex errorCode = CFErrorGetCode(error);
879 if(errorCode == kSOSErrorPublicKeyAbsent){
880 secnotice("cjr", "SA: We need the password to re-validate ourselves - it's changed on another device");
881 postKickedOutAlert(kSOSPasswordChanged);
882 state.lastCircleStatus = kSOSCCError;
883 [state writeToStorage];
887 // No longer in circle?
888 if ((state.lastCircleStatus == kSOSCCInCircle && (circleStatus == kSOSCCNotInCircle || circleStatus == kSOSCCCircleAbsent)) ||
889 (state.lastCircleStatus == kSOSCCCircleAbsent && circleStatus == kSOSCCNotInCircle && state.absentCircleWithNoReason) ||
890 state.debugShowLeftReason) {
891 // Used to be in the circle, now we aren't - tell the user why
892 debugState = @"processEvents B";
894 if (state.debugShowLeftReason) {
895 secnotice("cjr", "debugShowLeftReason: %@", state.debugShowLeftReason);
896 departureReason = [state.debugShowLeftReason intValue];
897 state.debugShowLeftReason = nil;
898 CFReleaseNull(departError);
899 [state writeToStorage];
902 if (departureReason != kSOSDepartureReasonError) {
903 state.absentCircleWithNoReason = (circleStatus == kSOSCCCircleAbsent && departureReason == kSOSNeverLeftCircle);
904 secnotice("cjr", "Depature reason %d", departureReason);
906 secnotice("cjr", "posting revocation notification!");
907 postKickedOutAlert(departureReason);
909 else if(_isAccountICDP && _hasPostedFollowupAndStillInError == false){
910 NSError *localError = nil;
911 CDPFollowUpController *cdpd = [[CDPFollowUpController alloc] init];
912 CDPFollowUpContext *context = [CDPFollowUpContext contextForStateRepair];
913 [cdpd postFollowUpWithContext:context error:&localError ];
915 secnotice("cjr", "request to CoreCDP to follow up failed: %@", localError);
918 secnotice("cjr", "CoreCDP handling follow up");
919 _hasPostedFollowupAndStillInError = true;
923 secnotice("cjr", "still waiting for followup to resolve");
925 secnotice("cjr", "pKOA returned (cS %d lCS %d)", circleStatus, state.lastCircleStatus);
927 secnotice("cjr", "Couldn't get last departure reason: %@", departError);
932 // Circle applications: pending request(s) started / completed
933 debugState = @"processEvents C";
934 if (circleStatus != state.lastCircleStatus) {
935 SOSCCStatus lastCircleStatus = state.lastCircleStatus;
936 state.lastCircleStatus = circleStatus;
938 if (lastCircleStatus != kSOSCCRequestPending && circleStatus == kSOSCCRequestPending) {
939 secnotice("cjr", "Pending request started");
940 state.applicationDate = nowish;
941 state.pendingApplicationReminder = [nowish dateByAddingTimeInterval: state.pendingApplicationReminderAlertInterval];
942 scheduleActivity(state.pendingApplicationReminderAlertInterval);
944 if (lastCircleStatus == kSOSCCRequestPending && circleStatus != kSOSCCRequestPending) {
945 secnotice("cjr", "Pending request completed");
946 state.applicationDate = [NSDate distantPast];
947 state.pendingApplicationReminder = [NSDate distantFuture];
950 [state writeToStorage];
953 if (circleStatus != kSOSCCInCircle) {
954 if (circleStatus == kSOSCCRequestPending && currentAlert) {
956 CFUserNotificationRef postedAlert = currentAlert;
958 debugState = @"processEvents D1";
959 notify_register_dispatch(kSOSCCCircleChangedNotification, ¬ifyToken, dispatch_get_main_queue(), ^(int token) {
960 if (postedAlert != currentAlert) {
961 secnotice("cjr", "-- CC after original alert gone (currentAlertIsForApplicants %d, pA %p, cA %p -- %@)",
962 currentAlertIsForApplicants, postedAlert, currentAlert, currentAlert);
963 notify_cancel(token);
965 CFErrorRef localError = NULL;
966 SOSCCStatus newCircleStatus = SOSCCThisDeviceIsInCircle(&localError);
967 BOOL xpcError = isErrorFromXPC(localError);
968 if(xpcError && newCircleStatus == kSOSCCError) {
969 secnotice("cjr", "returning from processEvents due to error returned from securityd: %@", localError);
973 if (newCircleStatus != kSOSCCRequestPending) {
974 if (newCircleStatus == kSOSCCError)
975 secnotice("cjr", "No longer pending (nCS=%d, alert=%@) error: %@", newCircleStatus, currentAlert, localError);
977 secnotice("cjr", "No longer pending (nCS=%d, alert=%@)", newCircleStatus, currentAlert);
978 cancelCurrentAlert(true);
980 secnotice("cjr", "Still pending...");
982 CFReleaseNull(localError);
985 debugState = @"processEvents D2";
986 secnotice("cjr", "NOTE: currentAlertIsForApplicants %d, token %d", currentAlertIsForApplicants, notifyToken);
990 debugState = @"processEvents D4";
991 secnotice("cjr", "SOSCCThisDeviceIsInCircle status %d, not checking applicants", circleStatus);
997 debugState = @"processEvents E";
998 applicants = [NSMutableDictionary new];
999 for (id applicantInfo in (__bridge_transfer NSArray *) SOSCCCopyApplicantPeerInfo(&error)) {
1000 Applicant *applicant = [[Applicant alloc] initWithPeerInfo:(__bridge SOSPeerInfoRef) applicantInfo];
1001 applicants[applicant.idString] = applicant;
1004 // Log error from SOSCCCopyApplicantPeerInfo() above?
1005 CFReleaseNull(error);
1007 int notify_token = -42;
1008 debugState = @"processEvents F";
1009 int notify_register_status = notify_register_dispatch(kSOSCCCircleChangedNotification, ¬ify_token, dispatch_get_main_queue(), ^(int token) {
1010 secnotice("cjr", "Notified: %s", kSOSCCCircleChangedNotification);
1011 CFErrorRef circleStatusError = NULL;
1013 bool needsUpdate = false;
1014 CFErrorRef copyPeerError = NULL;
1015 NSMutableSet *newIds = [NSMutableSet new];
1016 for (id applicantInfo in (__bridge_transfer NSArray *) SOSCCCopyApplicantPeerInfo(©PeerError)) {
1017 Applicant *newApplicant = [[Applicant alloc] initWithPeerInfo:(__bridge SOSPeerInfoRef) applicantInfo];
1018 [newIds addObject:newApplicant.idString];
1019 Applicant *existingApplicant = applicants[newApplicant.idString];
1020 if (existingApplicant) {
1021 switch (existingApplicant.applicantUIState) {
1022 case ApplicantWaiting:
1023 applicants[newApplicant.idString] = newApplicant;
1026 case ApplicantOnScreen:
1027 newApplicant.applicantUIState = ApplicantOnScreen;
1028 applicants[newApplicant.idString] = newApplicant;
1032 secnotice("cjr", "Update to %@ >> %@ with pending order, should work out ok though", existingApplicant, newApplicant);
1037 applicants[newApplicant.idString] = newApplicant;
1040 if (copyPeerError) {
1041 secnotice("cjr", "Could not update peer info array: %@", copyPeerError);
1042 CFRelease(copyPeerError);
1046 NSMutableArray *idsToRemoveFromApplicants = [NSMutableArray new];
1047 for (NSString *exisitngId in [applicants keyEnumerator]) {
1048 if (![newIds containsObject:exisitngId]) {
1049 [idsToRemoveFromApplicants addObject:exisitngId];
1053 [applicants removeObjectsForKeys:idsToRemoveFromApplicants];
1055 if (newIds.count == 0) {
1056 secnotice("cjr", "All applicants were handled elsewhere");
1057 cancelCurrentAlert(true);
1059 CFErrorRef circleError = NULL;
1060 SOSCCStatus currentCircleStatus = SOSCCThisDeviceIsInCircle(&circleError);
1061 BOOL xpcError = isErrorFromXPC(circleError);
1062 if(xpcError && currentCircleStatus == kSOSCCError) {
1063 secnotice("cjr", "returning early due to error returned from securityd: %@", circleError);
1066 if (kSOSCCInCircle != currentCircleStatus) {
1067 secnotice("cjr", "Left circle (%d), not handling remaining %lu applicants", currentCircleStatus, (unsigned long)newIds.count);
1068 cancelCurrentAlert(true);
1073 secnotice("cjr", "needsUpdate false, not updating alert");
1075 // Log circleStatusError?
1076 CFReleaseNull(circleStatusError);
1078 secnotice("cjr", "ACC token %d, status %d", notify_token, notify_register_status);
1079 debugState = @"processEvents F2";
1081 if (applicants.count == 0) {
1082 secnotice("cjr", "No applicants");
1084 debugState = @"processEvents F3";
1086 debugState = @"processEvents F4";
1088 debugState = @"processEvents F5";
1093 debugState = @"processEvents F6";
1094 notify_cancel(notify_token);
1095 debugState = @"processEvents DONE";
1101 int main (int argc, const char * argv[]) {
1102 os_transaction_t txion = os_transaction_create("com.apple.security.circle-join-requested");
1106 // NOTE: DISPATCH_QUEUE_PRIORITY_LOW will not actually manage to drain events in a lot of cases (like circleStatus != kSOSCCInCircle)
1107 xpc_set_event_stream_handler("com.apple.notifyd.matching", dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^(xpc_object_t object) {
1108 char *event_description = xpc_copy_description(object);
1109 const char *notificationName = xpc_dictionary_get_string(object, "Notification");
1111 if (notificationName && strcmp(notificationName, kUserKeybagStateChangeNotification)==0) {
1112 secnotice("cjr", "keybag changed!");
1113 keybagStateChange();
1116 secnotice("cjr", "notifyd event: %s\nAlert (%p) %s %s\ndebugState: %@", event_description, currentAlert,
1117 currentAlertIsForApplicants ? "for applicants" : "!applicants",
1118 currentAlertIsForKickOut ? "KO" : "!KO", debugState);
1119 free(event_description);
1122 xpc_activity_register(kLaunchLaterXPCName, XPC_ACTIVITY_CHECK_IN, ^(xpc_activity_t activity) {
1125 int falseInARow = 0;
1126 while (falseInARow < 2 && !_executeProcessEventsOnce) {
1127 if (processEvents()) {
1128 secnotice("cjr", "Processed events!!!");
1133 cancelCurrentAlert(false);
1134 if (doOnceInMainBlockChain) {
1135 doOnceInMainBlockChain();
1136 doOnceInMainBlockChain = NULL;
1141 secnotice("cjr", "Done");
1142 (void) txion; // But we really do want this around, compiler...