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/ManagedConfiguration.h>
35 #import <MobileCoreServices/MobileCoreServices.h>
36 #import <MobileCoreServices/LSApplicationWorkspace.h>
37 #import <MobileGestalt.h>
38 #import <os/transaction_private.h>
39 #import <ProtectedCloudStorage/CloudIdentity.h>
40 #import <Security/SecFrameworkStrings.h>
41 #import <SpringBoardServices/SBSCFUserNotificationKeys.h>
42 #include <dispatch/dispatch.h>
43 #include "keychain/SecureObjectSync/SOSCloudCircle.h"
44 #include "keychain/SecureObjectSync/SOSCloudCircleInternal.h"
45 #include "keychain/SecureObjectSync/SOSPeerInfo.h"
46 #include "keychain/SecureObjectSync/SOSInternal.h"
50 #import "NSArray+map.h"
51 #import "PersistentState.h"
52 #include <xpc/private.h>
54 #import "NSDate+TimeIntervalDescription.h"
55 #include <xpc/activity.h>
56 #include <xpc/private.h>
57 #import "os/activity.h"
59 #include "utilities/SecCFRelease.h"
60 #include "utilities/debugging.h"
61 #include "utilities/SecAKSWrappers.h"
62 #include "utilities/SecCFWrappers.h"
63 #include <utilities/SecXPCError.h>
64 #import <os/variant_private.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];
147 secnotice("followup", "Posting a follow up (for SOS) of type repair");
148 [cdpd postFollowUpWithContext:context error:&localError ];
149 secnotice("cjr", "account is icdp");
151 secnotice("cjr", "request to CoreCDP to follow up failed: %@", localError);
154 secnotice("cjr", "CoreCDP handling follow up");
155 _hasPostedFollowupAndStillInError = true;
158 else if(_isAccountICDP && circleStatus == kSOSCCInCircle){
159 _hasPostedFollowupAndStillInError = false;
162 secnotice("cjr", "account not icdp");
166 static bool updateIsLocked ()
168 CFErrorRef aksError = NULL;
169 if (!SecAKSGetIsLocked(&_isLocked, &aksError)) {
171 secerror("Got error querying lock state: %@", aksError);
172 CFReleaseSafe(aksError);
176 _unlockedSinceBoot = YES;
180 static void keybagStateChange ()
182 secerror("osactivity initiated");
183 os_activity_initiate("keybagStateChanged", OS_ACTIVITY_FLAG_DEFAULT, ^{
184 BOOL wasLocked = _isLocked;
185 if ( updateIsLocked()) {
186 if (wasLocked == _isLocked)
187 secerror("still %s ignoring", _isLocked ? "locked" : "unlocked");
196 static void doOnceInMain(dispatch_block_t block)
198 if (doOnceInMainBlockChain) {
199 doOnceInMainBlockChain = ^{
200 doOnceInMainBlockChain();
204 doOnceInMainBlockChain = block;
209 static NSString *appleIDAccountName()
211 ACAccountStore *accountStore = [[ACAccountStore alloc] init];
212 ACAccount *primaryAppleAccount = [accountStore aa_primaryAppleAccount];
213 return primaryAppleAccount.username;
217 static CFOptionFlags flagsForAsk(Applicant *applicant)
219 return kCFUserNotificationPlainAlertLevel | CFUserNotificationSecureTextField(0);
223 // NOTE: gives precedence to OnScreen
224 static Applicant *firstApplicantWaitingOrOnScreen()
226 Applicant *waiting = nil;
227 for (Applicant *applicant in [applicants objectEnumerator]) {
228 if (applicant.applicantUIState == ApplicantOnScreen) {
230 } else if (applicant.applicantUIState == ApplicantWaiting) {
239 static NSMutableArray *applicantsInState(ApplicantUIState state)
241 NSMutableArray *results = [NSMutableArray new];
242 for (Applicant *applicant in [applicants objectEnumerator]) {
243 if (applicant.applicantUIState == state) {
244 [results addObject:applicant];
252 BOOL processRequests(CFErrorRef *error) {
253 NSMutableArray *toAccept = [[applicantsInState(ApplicantAccepted) mapWithBlock:^id(id obj) {return (id)[obj rawPeerInfo];}] mutableCopy];
254 NSMutableArray *toReject = [[applicantsInState(ApplicantRejected) mapWithBlock:^id(id obj) {return (id)[obj rawPeerInfo];}] mutableCopy];
257 if ([toAccept count]) {
258 secnotice("cjr", "Process accept: %@", toAccept);
259 ok = ok && SOSCCAcceptApplicants((__bridge CFArrayRef) toAccept, error);
261 secnotice("cjr", "kSOSCCHoldLockForInitialSync");
262 notify_post(kSOSCCHoldLockForInitialSync);
266 if ([toReject count]) {
267 secnotice("cjr", "Process reject: %@", toReject);
268 ok = ok && SOSCCRejectApplicants((__bridge CFArrayRef) toReject, error);
275 static void cancelCurrentAlert(bool stopRunLoop) {
276 if (currentAlertSource) {
277 CFRunLoopRemoveSource(CFRunLoopGetCurrent(), currentAlertSource, kCFRunLoopDefaultMode);
278 CFReleaseNull(currentAlertSource);
281 CFUserNotificationCancel(currentAlert);
282 CFReleaseNull(currentAlert);
285 CFRunLoopStop(CFRunLoopGetCurrent());
287 currentAlertIsForKickOut = currentAlertIsForApplicants = false;
291 static void askAboutAll(bool passwordFailure);
294 static void applicantChoice(CFUserNotificationRef userNotification, CFOptionFlags responseFlags)
296 ApplicantUIState choice;
298 if (kCFUserNotificationAlternateResponse == responseFlags) {
299 choice = ApplicantRejected;
300 } else if (kCFUserNotificationDefaultResponse == responseFlags) {
301 choice = ApplicantAccepted;
303 secnotice("cjr", "Unexpected response %lu", responseFlags);
304 choice = ApplicantRejected;
308 CFErrorRef error = NULL;
309 NSArray *onScreen = applicantsInState(ApplicantOnScreen);
311 [onScreen enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
312 Applicant* applicant = (Applicant *) obj;
313 applicant.applicantUIState = choice;
316 if (choice == ApplicantRejected) {
317 // If this device has ever set up the public key this should work without the password...
318 processed = processRequests(&error);
320 secnotice("cjr", "Didn't need password to process %@", onScreen);
321 cancelCurrentAlert(true);
324 // ...however if the public key gets lost we should "just" fall through to the validate
326 secnotice("cjr", "Couldn't process reject without password (e=%@) for %@ (will try with password next)", error, onScreen);
328 if(CFErrorIsMalfunctioningKeybagError(error)){
329 secnotice("cjr", "system is locked, dismiss the notification");
330 processApplicantsAfterUnlock = true;
334 CFReleaseNull(error);
337 NSString *password = (__bridge NSString *) CFUserNotificationGetResponseValue(userNotification, kCFUserNotificationTextFieldValuesKey, 0);
339 secnotice("cjr", "No password given, retry");
343 const char *passwordUTF8 = [password UTF8String];
344 NSData *passwordBytes = [NSData dataWithBytes:passwordUTF8 length:strlen(passwordUTF8)];
346 // Sometimes securityd crashes between SOSCCRegisterUserCredentials and processRequests
347 // (which results in a process error -- I think this is 13355140), as a workaround we retry
348 // failure a few times before we give up.
349 for (int try = 0; try < 5 && !processed; try++) {
350 if (!SOSCCTryUserCredentials(CFSTR(""), (__bridge CFDataRef) passwordBytes, &error)) {
351 secnotice("cjr", "Try user credentials failed %@", error);
352 if ((error == NULL) ||
353 (CFEqual(kSOSErrorDomain, CFErrorGetDomain(error)) && kSOSErrorWrongPassword == CFErrorGetCode(error))) {
354 secnotice("cjr", "Calling askAboutAll again...");
355 [onScreen enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
356 Applicant *applicant = (Applicant*) obj;
357 applicant.applicantUIState = ApplicantWaiting;
360 CFReleaseNull(error);
363 EXIT_LOGGED_FAILURE(EX_DATAERR);
366 processed = processRequests(&error);
368 secnotice("cjr", "Can't processRequests: %@ for %@", error, onScreen);
370 CFReleaseNull(error);
373 if (processed && firstApplicantWaitingOrOnScreen()) {
374 cancelCurrentAlert(false);
377 cancelCurrentAlert(true);
382 static void passwordFailurePrompt()
384 #pragma clang diagnostic push
385 #pragma clang diagnostic ignored "-Wformat-nonliteral"
386 NSString *pwIncorrect = [NSString stringWithFormat:(NSString *)CFBridgingRelease(SecCopyCKString(SEC_CK_PASSWORD_INCORRECT)), appleIDAccountName()];
387 #pragma clang diagnostic pop
388 NSString *tryAgain = CFBridgingRelease(SecCopyCKString(SEC_CK_TRY_AGAIN));
389 NSDictionary *noteAttributes = @{
390 (id) kCFUserNotificationAlertHeaderKey : pwIncorrect,
391 (id) kCFUserNotificationDefaultButtonTitleKey : tryAgain,
392 (id) kCFUserNotificationAlertTopMostKey : @YES, // get us onto the lock screen
393 (__bridge id) SBUserNotificationDontDismissOnUnlock: @YES,
394 (__bridge id) SBUserNotificationDismissOnLock : @NO,
396 CFOptionFlags flags = kCFUserNotificationPlainAlertLevel;
398 CFUserNotificationRef note = CFUserNotificationCreate(NULL, 0.0, flags, &err, (__bridge CFDictionaryRef) noteAttributes);
401 CFUserNotificationReceiveResponse(note, 0.0, &flags);
407 static NSString *getLocalizedApprovalBody(NSString *deviceType) {
408 CFStringRef applicationReminder = NULL;
410 if ([deviceType isEqualToString:@"iPhone"])
411 applicationReminder = SecCopyCKString(SEC_CK_APPROVAL_BODY_IOS_IPHONE);
412 else if ([deviceType isEqualToString:@"iPod"])
413 applicationReminder = SecCopyCKString(SEC_CK_APPROVAL_BODY_IOS_IPOD);
414 else if ([deviceType isEqualToString:@"iPad"])
415 applicationReminder = SecCopyCKString(SEC_CK_APPROVAL_BODY_IOS_IPAD);
416 else if ([deviceType isEqualToString:@"Mac"])
417 applicationReminder = SecCopyCKString(SEC_CK_APPROVAL_BODY_IOS_MAC);
419 applicationReminder = SecCopyCKString(SEC_CK_APPROVAL_BODY_IOS_GENERIC);
421 return (__bridge_transfer NSString *) applicationReminder;
425 static NSDictionary *createNote(Applicant *applicantToAskAbout)
427 if(!applicantToAskAbout || !applicantToAskAbout.name || !applicantToAskAbout.deviceType)
430 #pragma clang diagnostic push
431 #pragma clang diagnostic ignored "-Wformat-nonliteral"
432 NSString *header = [NSString stringWithFormat: (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_APPROVAL_TITLE), applicantToAskAbout.name];
433 NSString *body = [NSString stringWithFormat: getLocalizedApprovalBody(applicantToAskAbout.deviceType), appleIDAccountName()];
434 #pragma clang diagnostic pop
437 (id) kCFUserNotificationAlertHeaderKey : header,
438 (id) kCFUserNotificationAlertMessageKey : body,
439 (id) kCFUserNotificationDefaultButtonTitleKey : (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_APPROVE),
440 (id) kCFUserNotificationAlternateButtonTitleKey: (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_DECLINE),
441 (id) kCFUserNotificationTextFieldTitlesKey : (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_ICLOUD_PASSWORD),
442 (id) kCFUserNotificationAlertTopMostKey : @YES, // get us onto the lock screen
443 (__bridge_transfer id) SBUserNotificationDontDismissOnUnlock: @YES,
444 (__bridge_transfer id) SBUserNotificationDismissOnLock : @NO,
449 static void askAboutAll(bool passwordFailure)
451 if ([[MCProfileConnection sharedConnection] effectiveBoolValueForSetting: MCFeatureAccountModificationAllowed] == MCRestrictedBoolExplicitNo) {
452 secnotice("cjr", "Account modifications not allowed.");
456 if (passwordFailure) {
457 passwordFailurePrompt();
460 if ((passwordFailure || !currentAlertIsForApplicants) && currentAlert) {
461 if (!currentAlertIsForApplicants) {
462 CFUserNotificationCancel(currentAlert);
464 // after password failure we need to remove the existing alert and supporting objects
465 // because we can't reuse them.
466 CFReleaseNull(currentAlert);
467 CFReleaseNull(currentAlertSource);
469 currentAlertIsForApplicants = true;
471 Applicant *applicantToAskAbout = firstApplicantWaitingOrOnScreen();
472 secnotice("cjr", "Asking about: %@ (of: %@)", applicantToAskAbout, applicants);
474 NSDictionary *noteAttributes = createNote(applicantToAskAbout);
475 if(!noteAttributes) {
476 secnotice("cjr", "NULL data for %@", applicantToAskAbout);
477 cancelCurrentAlert(true);
481 CFOptionFlags flags = flagsForAsk(applicantToAskAbout);
484 SInt32 err = CFUserNotificationUpdate(currentAlert, 0, flags, (__bridge CFDictionaryRef) noteAttributes);
486 secnotice("cjr", "CFUserNotificationUpdate err=%d", (int)err);
487 EXIT_LOGGED_FAILURE(EX_SOFTWARE);
491 currentAlert = CFUserNotificationCreate(NULL, 0.0, flags, &err, (__bridge CFDictionaryRef) noteAttributes);
493 secnotice("cjr", "Can't make notification for %@ err=%x", applicantToAskAbout, (int)err);
494 EXIT_LOGGED_FAILURE(EX_SOFTWARE);
497 currentAlertSource = CFUserNotificationCreateRunLoopSource(NULL, currentAlert, applicantChoice, 0);
498 CFRunLoopAddSource(CFRunLoopGetCurrent(), currentAlertSource, kCFRunLoopDefaultMode);
501 applicantToAskAbout.applicantUIState = ApplicantOnScreen;
505 static void scheduleActivity(int alertInterval)
507 xpc_object_t options = xpc_dictionary_create(NULL, NULL, 0);
508 xpc_dictionary_set_uint64(options, XPC_ACTIVITY_DELAY, alertInterval);
509 xpc_dictionary_set_uint64(options, XPC_ACTIVITY_GRACE_PERIOD, XPC_ACTIVITY_INTERVAL_1_MIN);
510 xpc_dictionary_set_bool(options, XPC_ACTIVITY_REPEATING, false);
511 xpc_dictionary_set_bool(options, XPC_ACTIVITY_ALLOW_BATTERY, true);
512 xpc_dictionary_set_string(options, XPC_ACTIVITY_PRIORITY, XPC_ACTIVITY_PRIORITY_UTILITY);
514 xpc_activity_register(kLaunchLaterXPCName, options, ^(xpc_activity_t activity) {
515 secnotice("cjr", "activity handler fired");
520 static void reminderChoice(CFUserNotificationRef userNotification, CFOptionFlags responseFlags) {
521 if (responseFlags == kCFUserNotificationAlternateResponse || responseFlags == kCFUserNotificationDefaultResponse) {
522 PersistentState *state = [PersistentState loadFromStorage];
523 NSDate *nowish = [NSDate new];
524 state.pendingApplicationReminder = [nowish dateByAddingTimeInterval: state.pendingApplicationReminderAlertInterval];
525 scheduleActivity(state.pendingApplicationReminderAlertInterval);
526 [state writeToStorage];
527 if (responseFlags == kCFUserNotificationAlternateResponse) {
529 BOOL ok = [[LSApplicationWorkspace defaultWorkspace] openSensitiveURL:[NSURL URLWithString:castleKeychainUrl] withOptions:nil];
530 secnotice("cjr", "%s iCSC: opening %@ ok=%d", __FUNCTION__, castleKeychainUrl, ok);
534 cancelCurrentAlert(true);
538 static bool iCloudResetAvailable() {
539 SecureBackup *backupd = [SecureBackup new];
540 NSDictionary *backupdResults;
541 NSError *error = [backupd getAccountInfoWithInfo:nil results:&backupdResults];
542 secnotice("cjr", "SecureBackup e=%@ r=%@", error, backupdResults);
543 return (error == nil && [backupdResults[kSecureBackupIsEnabledKey] isEqualToNumber:@YES]);
547 static NSString *getLocalizedApplicationReminder() {
548 CFStringRef applicationReminder = NULL;
549 switch (MGGetSInt32Answer(kMGQDeviceClassNumber, MGDeviceClassInvalid)) {
550 case MGDeviceClassiPhone:
551 applicationReminder = SecCopyCKString(SEC_CK_REMINDER_BODY_IOS_IPHONE);
553 case MGDeviceClassiPod:
554 applicationReminder = SecCopyCKString(SEC_CK_REMINDER_BODY_IOS_IPOD);
556 case MGDeviceClassiPad:
557 applicationReminder = SecCopyCKString(SEC_CK_REMINDER_BODY_IOS_IPAD);
560 applicationReminder = SecCopyCKString(SEC_CK_REMINDER_BODY_IOS_GENERIC);
563 return (__bridge_transfer NSString *) applicationReminder;
566 static bool isSOSInternalDevice(void) {
567 static dispatch_once_t onceToken;
568 static BOOL internal = NO;
569 dispatch_once(&onceToken, ^{
570 internal = os_variant_has_internal_diagnostics("com.apple.security");
575 static void postApplicationReminderAlert(NSDate *nowish, PersistentState *state, unsigned int alertInterval)
577 NSString *body = getLocalizedApplicationReminder();
578 bool has_iCSC = iCloudResetAvailable();
580 if (isSOSInternalDevice() &&
581 state.defaultPendingApplicationReminderAlertInterval != state.pendingApplicationReminderAlertInterval) {
583 body = [body stringByAppendingFormat: @"〖debug interval %u; wait time %@〗",
584 state.pendingApplicationReminderAlertInterval,
585 [nowish copyDescriptionOfIntervalSince:state.applicationDate]];
589 NSDictionary *pendingAttributes = @{
590 (id) kCFUserNotificationAlertHeaderKey : CFBridgingRelease(SecCopyCKString(SEC_CK_REMINDER_TITLE_IOS)),
591 (id) kCFUserNotificationAlertMessageKey : body,
592 (id) kCFUserNotificationDefaultButtonTitleKey : CFBridgingRelease(SecCopyCKString(SEC_CK_REMINDER_BUTTON_OK)),
593 (id) kCFUserNotificationAlternateButtonTitleKey: has_iCSC ? CFBridgingRelease(SecCopyCKString(SEC_CK_REMINDER_BUTTON_ICSC)) : @"",
594 (id) kCFUserNotificationAlertTopMostKey : @YES,
595 (__bridge id) SBUserNotificationDontDismissOnUnlock : @YES,
596 (__bridge id) SBUserNotificationDismissOnLock : @NO,
599 currentAlert = CFUserNotificationCreate(NULL, 0.0, kCFUserNotificationPlainAlertLevel, &err, (__bridge CFDictionaryRef) pendingAttributes);
602 secnotice("cjr", "Can't make pending notification err=%x", (int)err);
604 currentAlertIsForApplicants = false;
605 currentAlertSource = CFUserNotificationCreateRunLoopSource(NULL, currentAlert, reminderChoice, 0);
606 CFRunLoopAddSource(CFRunLoopGetCurrent(), currentAlertSource, kCFRunLoopDefaultMode);
611 static void kickOutChoice(CFUserNotificationRef userNotification, CFOptionFlags responseFlags) {
612 secnotice("cjr", "kOC %@ %lu", userNotification, responseFlags);
614 //default response: continue -> settings pref pane advanced keychain sync page
615 if (responseFlags == kCFUserNotificationDefaultResponse) {
616 // We need to let things unwind to main for the new state to get saved
618 NSURL *url = [NSURL URLWithString: _isAccountICDP ? rejoinICDPUrl : castleKeychainUrl];
619 BOOL ok = [[LSApplicationWorkspace defaultWorkspace] openSensitiveURL:url withOptions:nil];
620 secnotice("cjr","kickOutChoice account is iCDP: %d", _isAccountICDP);
621 secnotice("cjr", "ok=%d opening %@", ok, url);
624 //alternate response: later -> call CD
625 else if (responseFlags == kCFUserNotificationAlternateResponse) {
626 // We need to let things unwind to main for the new state to get saved
629 CDPFollowUpController *cdpd = [[CDPFollowUpController alloc] init];
630 NSError *localError = nil;
631 CDPFollowUpContext *context = [CDPFollowUpContext contextForStateRepair];
633 secnotice("followup", "Posting a follow up (for SOS) of type repair");
634 [cdpd postFollowUpWithContext:context error:&localError ];
636 secnotice("cjr", "request to CoreCDP to follow up failed: %@", localError);
639 secnotice("cjr", "CoreCDP handling follow up");
640 _hasPostedFollowupAndStillInError = true;
646 cancelCurrentAlert(true);
650 CFStringRef const CJRAggdDepartureReasonKey = CFSTR("com.apple.security.circlejoinrequested.departurereason");
651 CFStringRef const CJRAggdNumCircleDevicesKey = CFSTR("com.apple.security.circlejoinrequested.numcircledevices");
653 static void postKickedOutAlert(enum DepartureReason reason)
655 NSString *header = nil;
656 NSString *message = nil;
658 // <rdar://problem/20358568> iCDP telemetry: ☂ Statistics on circle reset and drop out events
659 ADClientSetValueForScalarKey(CJRAggdDepartureReasonKey, reason);
661 int64_t num_peers = 0;
662 CFArrayRef peerList = SOSCCCopyPeerPeerInfo(NULL);
664 num_peers = CFArrayGetCount(peerList);
665 if (num_peers > 99) {
666 // Round down # peers to 2 significant digits
668 for (factor = 10; num_peers >= 100*factor; factor *= 10) ;
669 num_peers = (num_peers / factor) * factor;
673 ADClientSetValueForScalarKey(CJRAggdNumCircleDevicesKey, num_peers);
675 debugState = @"pKOA A";
676 secnotice("cjr", "DepartureReason %d", reason);
678 case kSOSDiscoveredRetirement:
679 case kSOSLostPrivateKey:
680 case kSOSWithdrewMembership:
681 // Was: SEC_CK_CR_BODY_WITHDREW
682 // "... if you turn off a switch you have some idea why the light is off" - Murf
685 case kSOSNeverAppliedToCircle:
686 // We didn't get kicked out, we were never here. This should only happen if we changed iCloud accounts
687 // (and we had sync on in the previous one, and never had it on in the new one). As this is explicit
688 // user action alot of the "Light switch" argument (above) applies.
691 case kSOSPasswordChanged:
692 case kSOSNeverLeftCircle:
693 case kSOSMembershipRevoked:
694 case kSOSLeftUntrustedCircle:
696 header = (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_PWD_REQUIRED_TITLE);
697 message = (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_PWD_REQUIRED_BODY_IOS);
701 if (isSOSInternalDevice()) {
702 static const char *departureReasonStrings[] = {
703 "kSOSDepartureReasonError",
704 "kSOSNeverLeftCircle",
705 "kSOSWithdrewMembership",
706 "kSOSMembershipRevoked",
707 "kSOSLeftUntrustedCircle",
708 "kSOSNeverAppliedToCircle",
709 "kSOSDiscoveredRetirement",
710 "kSOSLostPrivateKey",
711 "kSOSPasswordChanged",
714 int idx = (kSOSDepartureReasonError <= reason && reason <= kSOSLostPrivateKey) ? reason : (kSOSLostPrivateKey + 1);
715 #pragma clang diagnostic push
716 #pragma clang diagnostic ignored "-Wformat-nonliteral"
717 NSString *reason_str = [NSString stringWithFormat:(__bridge_transfer NSString *) SecCopyCKString(SEC_CK_CR_REASON_INTERNAL),
718 departureReasonStrings[idx]];
719 #pragma clang diagnostic pop
720 message = [message stringByAppendingString: reason_str];
723 NSDictionary *kickedAttributes = @{
724 (id) kCFUserNotificationAlertHeaderKey : header,
725 (id) kCFUserNotificationAlertMessageKey : message,
726 (id) kCFUserNotificationDefaultButtonTitleKey : (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_CONTINUE),
727 (id) kCFUserNotificationAlternateButtonTitleKey: (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_NOT_NOW),
728 (id) kCFUserNotificationAlertTopMostKey : @YES,
729 (__bridge id) SBUserNotificationDismissOnLock : @NO,
730 (__bridge id) SBUserNotificationDontDismissOnUnlock : @YES,
734 if (currentAlertIsForKickOut) {
735 debugState = @"pKOA B";
736 secnotice("cjr", "Updating existing alert %@ with %@", currentAlert, kickedAttributes);
737 CFUserNotificationUpdate(currentAlert, 0, kCFUserNotificationPlainAlertLevel, (__bridge CFDictionaryRef) kickedAttributes);
739 debugState = @"pKOA C";
741 CFUserNotificationRef note = CFUserNotificationCreate(NULL, 0.0, kCFUserNotificationPlainAlertLevel, &err, (__bridge CFDictionaryRef) kickedAttributes);
742 assert((note == NULL) == (err != 0));
744 secnotice("cjr", "Can't make kicked out notification err=%x", (int)err);
747 currentAlertIsForApplicants = false;
748 currentAlertIsForKickOut = true;
751 secnotice("cjr", "New ko alert %@ a=%@", currentAlert, kickedAttributes);
752 currentAlertSource = CFUserNotificationCreateRunLoopSource(NULL, currentAlert, kickOutChoice, 0);
753 CFRunLoopAddSource(CFRunLoopGetCurrent(), currentAlertSource, kCFRunLoopDefaultMode);
754 int backupStateChangeToken;
755 notify_register_dispatch("com.apple.EscrowSecurityAlert.reset", &backupStateChangeToken, dispatch_get_main_queue(), ^(int token) {
756 if (currentAlert == note) {
757 secnotice("cjr", "Backup state might have changed (dS=%@)", debugState);
758 postKickedOutAlert(reason);
760 secnotice("cjr", "Backup state may have changed, but we don't care anymore (dS=%@)", debugState);
764 debugState = @"pKOA D";
766 debugState = @"pKOA E";
767 notify_cancel(backupStateChangeToken);
770 debugState = @"pKOA Z";
773 static void askForCDPFollowup() {
775 NSError *localError = nil;
776 CDPFollowUpController *cdpd = [[CDPFollowUpController alloc] init];
777 CDPFollowUpContext *context = [CDPFollowUpContext contextForStateRepair];
779 secnotice("followup", "Posting a follow up (for SOS) of type repair");
780 [cdpd postFollowUpWithContext:context error:&localError ];
782 secnotice("cjr", "request to CoreCDP to follow up failed: %@", localError);
785 secnotice("cjr", "CoreCDP handling follow up");
786 _hasPostedFollowupAndStillInError = true;
791 static bool processEvents()
793 debugState = @"processEvents A";
795 CFErrorRef error = NULL;
796 CFErrorRef departError = NULL;
797 SOSCCStatus circleStatus = SOSCCThisDeviceIsInCircleNonCached(&error);
798 enum DepartureReason departureReason = SOSCCGetLastDepartureReason(&departError);
800 BOOL abortFromError = isErrorFromXPC(error);
801 if(abortFromError && circleStatus == kSOSCCError) {
802 secnotice("cjr", "returning from processEvents due to error returned from securityd: %@", error);
805 if (departureReason == kSOSDepartureReasonError && departError && (CFEqual(sSecXPCErrorDomain, CFErrorGetDomain(departError)))) {
806 secnotice("cjr", "XPC error while checking last departure reason: \"%@\", not processing events", departError);
810 NSDate *nowish = [NSDate date];
811 PersistentState *state = [PersistentState loadFromStorage];
812 secnotice("cjr", "CircleStatus %d -> %d{%d} (s=%p)", state.lastCircleStatus, circleStatus, departureReason, state);
814 // Pending application reminder
815 NSTimeInterval timeUntilApplicationAlert = [state.pendingApplicationReminder timeIntervalSinceDate:nowish];
816 secnotice("cjr", "Time until pendingApplicationReminder (%@) %f", [state.pendingApplicationReminder debugDescription], timeUntilApplicationAlert);
817 if (circleStatus == kSOSCCRequestPending) {
818 if (timeUntilApplicationAlert <= 0) {
819 debugState = @"reminderAlert";
820 postApplicationReminderAlert(nowish, state, state.pendingApplicationReminderAlertInterval);
822 scheduleActivity(ceil(timeUntilApplicationAlert));
826 PSKeychainSyncIsUsingICDP();
828 // Refresh because sometimes we're fixed elsewhere before we get here.
829 CFReleaseNull(error);
830 circleStatus = SOSCCThisDeviceIsInCircleNonCached(&error);
831 abortFromError = isErrorFromXPC(error);
832 if(abortFromError && circleStatus == kSOSCCError) {
833 secnotice("cjr", "returning from processEvents due to error returned from securityd: %@", error);
838 state.lastCircleStatus = circleStatus;
839 [state writeToStorage];
840 if(_hasPostedFollowupAndStillInError == true) {
841 secnotice("cjr", "followup not resolved");
842 _executeProcessEventsOnce = true;
846 switch(circleStatus) {
848 secnotice("cjr", "follow up should be resolved");
849 _executeProcessEventsOnce = true;
850 _hasPostedFollowupAndStillInError = false;
853 secnotice("cjr", "error from SOSCCThisDeviceIsInCircle: %@", error);
855 _executeProcessEventsOnce = true;
857 case kSOSCCCircleAbsent:
858 case kSOSCCNotInCircle:
860 You would think we could count on not being iCDP if the account was signed out. Evidently that's wrong.
861 So we'll go based on the artifact that when the account object is reset (like by signing out) the
862 departureReason will be set to kSOSDepartureReasonError. So we won't push to get back into a circle if that's
863 the current reason. I've checked code for other ways we could be out. If we boot and can't load the account
864 we'll end up with kSOSDepartureReasonError. Then too if we end up in kSOSDepartureReasonError and reboot we end up
865 in the same place. Leave it to cdpd to decide whether the user needs to sign in to an account.
867 if(departureReason != kSOSDepartureReasonError) {
868 secnotice("cjr", "iCDP: We need to get back into the circle");
871 secnotice("cjr", "iCDP: We appear to not be associated with an iCloud account");
873 _executeProcessEventsOnce = true;
875 case kSOSCCRequestPending:
878 secnotice("cjr", "Unknown circle status %d", circleStatus);
881 } else if(circleStatus == kSOSCCError && state.lastCircleStatus != kSOSCCError && (departureReason == kSOSNeverLeftCircle)) {
882 secnotice("cjr", "SA: error from SOSCCThisDeviceIsInCircle: %@", error);
883 CFIndex errorCode = CFErrorGetCode(error);
884 if(errorCode == kSOSErrorPublicKeyAbsent){
885 secnotice("cjr", "SA: We need the password to re-validate ourselves - it's changed on another device");
886 postKickedOutAlert(kSOSPasswordChanged);
887 state.lastCircleStatus = kSOSCCError;
888 [state writeToStorage];
892 // No longer in circle?
893 if ((state.lastCircleStatus == kSOSCCInCircle && (circleStatus == kSOSCCNotInCircle || circleStatus == kSOSCCCircleAbsent)) ||
894 (state.lastCircleStatus == kSOSCCCircleAbsent && circleStatus == kSOSCCNotInCircle && state.absentCircleWithNoReason) ||
895 state.debugShowLeftReason) {
896 // Used to be in the circle, now we aren't - tell the user why
897 debugState = @"processEvents B";
899 if (state.debugShowLeftReason) {
900 secnotice("cjr", "debugShowLeftReason: %@", state.debugShowLeftReason);
901 departureReason = [state.debugShowLeftReason intValue];
902 state.debugShowLeftReason = nil;
903 CFReleaseNull(departError);
904 [state writeToStorage];
907 if (departureReason != kSOSDepartureReasonError) {
908 state.absentCircleWithNoReason = (circleStatus == kSOSCCCircleAbsent && departureReason == kSOSNeverLeftCircle);
909 secnotice("cjr", "Depature reason %d", departureReason);
911 secnotice("cjr", "posting revocation notification!");
912 postKickedOutAlert(departureReason);
914 else if(_isAccountICDP && _hasPostedFollowupAndStillInError == false){
915 NSError *localError = nil;
916 CDPFollowUpController *cdpd = [[CDPFollowUpController alloc] init];
917 CDPFollowUpContext *context = [CDPFollowUpContext contextForStateRepair];
919 secnotice("followup", "Posting a follow up (for SOS) of type repair");
920 [cdpd postFollowUpWithContext:context error:&localError ];
922 secnotice("cjr", "request to CoreCDP to follow up failed: %@", localError);
925 secnotice("cjr", "CoreCDP handling follow up");
926 _hasPostedFollowupAndStillInError = true;
930 secnotice("cjr", "still waiting for followup to resolve");
932 secnotice("cjr", "pKOA returned (cS %d lCS %d)", circleStatus, state.lastCircleStatus);
934 secnotice("cjr", "Couldn't get last departure reason: %@", departError);
939 // Circle applications: pending request(s) started / completed
940 debugState = @"processEvents C";
941 if (circleStatus != state.lastCircleStatus) {
942 SOSCCStatus lastCircleStatus = state.lastCircleStatus;
943 state.lastCircleStatus = circleStatus;
945 if (lastCircleStatus != kSOSCCRequestPending && circleStatus == kSOSCCRequestPending) {
946 secnotice("cjr", "Pending request started");
947 state.applicationDate = nowish;
948 state.pendingApplicationReminder = [nowish dateByAddingTimeInterval: state.pendingApplicationReminderAlertInterval];
949 scheduleActivity(state.pendingApplicationReminderAlertInterval);
951 if (lastCircleStatus == kSOSCCRequestPending && circleStatus != kSOSCCRequestPending) {
952 secnotice("cjr", "Pending request completed");
953 state.applicationDate = [NSDate distantPast];
954 state.pendingApplicationReminder = [NSDate distantFuture];
957 [state writeToStorage];
960 if (circleStatus != kSOSCCInCircle) {
961 if (circleStatus == kSOSCCRequestPending && currentAlert) {
963 CFUserNotificationRef postedAlert = currentAlert;
965 debugState = @"processEvents D1";
966 notify_register_dispatch(kSOSCCCircleChangedNotification, ¬ifyToken, dispatch_get_main_queue(), ^(int token) {
967 if (postedAlert != currentAlert) {
968 secnotice("cjr", "-- CC after original alert gone (currentAlertIsForApplicants %d, pA %p, cA %p -- %@)",
969 currentAlertIsForApplicants, postedAlert, currentAlert, currentAlert);
970 notify_cancel(token);
972 CFErrorRef localError = NULL;
973 SOSCCStatus newCircleStatus = SOSCCThisDeviceIsInCircle(&localError);
974 BOOL xpcError = isErrorFromXPC(localError);
975 if(xpcError && newCircleStatus == kSOSCCError) {
976 secnotice("cjr", "returning from processEvents due to error returned from securityd: %@", localError);
980 if (newCircleStatus != kSOSCCRequestPending) {
981 if (newCircleStatus == kSOSCCError)
982 secnotice("cjr", "No longer pending (nCS=%d, alert=%@) error: %@", newCircleStatus, currentAlert, localError);
984 secnotice("cjr", "No longer pending (nCS=%d, alert=%@)", newCircleStatus, currentAlert);
985 cancelCurrentAlert(true);
987 secnotice("cjr", "Still pending...");
989 CFReleaseNull(localError);
992 debugState = @"processEvents D2";
993 secnotice("cjr", "NOTE: currentAlertIsForApplicants %d, token %d", currentAlertIsForApplicants, notifyToken);
997 debugState = @"processEvents D4";
998 secnotice("cjr", "SOSCCThisDeviceIsInCircle status %d, not checking applicants", circleStatus);
1004 debugState = @"processEvents E";
1005 applicants = [NSMutableDictionary new];
1006 for (id applicantInfo in (__bridge_transfer NSArray *) SOSCCCopyApplicantPeerInfo(&error)) {
1007 Applicant *applicant = [[Applicant alloc] initWithPeerInfo:(__bridge SOSPeerInfoRef) applicantInfo];
1008 applicants[applicant.idString] = applicant;
1011 // Log error from SOSCCCopyApplicantPeerInfo() above?
1012 CFReleaseNull(error);
1014 int notify_token = -42;
1015 debugState = @"processEvents F";
1016 int notify_register_status = notify_register_dispatch(kSOSCCCircleChangedNotification, ¬ify_token, dispatch_get_main_queue(), ^(int token) {
1017 secnotice("cjr", "Notified: %s", kSOSCCCircleChangedNotification);
1018 CFErrorRef circleStatusError = NULL;
1020 bool needsUpdate = false;
1021 CFErrorRef copyPeerError = NULL;
1022 NSMutableSet *newIds = [NSMutableSet new];
1023 for (id applicantInfo in (__bridge_transfer NSArray *) SOSCCCopyApplicantPeerInfo(©PeerError)) {
1024 Applicant *newApplicant = [[Applicant alloc] initWithPeerInfo:(__bridge SOSPeerInfoRef) applicantInfo];
1025 [newIds addObject:newApplicant.idString];
1026 Applicant *existingApplicant = applicants[newApplicant.idString];
1027 if (existingApplicant) {
1028 switch (existingApplicant.applicantUIState) {
1029 case ApplicantWaiting:
1030 applicants[newApplicant.idString] = newApplicant;
1033 case ApplicantOnScreen:
1034 newApplicant.applicantUIState = ApplicantOnScreen;
1035 applicants[newApplicant.idString] = newApplicant;
1039 secnotice("cjr", "Update to %@ >> %@ with pending order, should work out ok though", existingApplicant, newApplicant);
1044 applicants[newApplicant.idString] = newApplicant;
1047 if (copyPeerError) {
1048 secnotice("cjr", "Could not update peer info array: %@", copyPeerError);
1049 CFRelease(copyPeerError);
1053 NSMutableArray *idsToRemoveFromApplicants = [NSMutableArray new];
1054 for (NSString *exisitngId in [applicants keyEnumerator]) {
1055 if (![newIds containsObject:exisitngId]) {
1056 [idsToRemoveFromApplicants addObject:exisitngId];
1060 [applicants removeObjectsForKeys:idsToRemoveFromApplicants];
1062 if (newIds.count == 0) {
1063 secnotice("cjr", "All applicants were handled elsewhere");
1064 cancelCurrentAlert(true);
1066 CFErrorRef circleError = NULL;
1067 SOSCCStatus currentCircleStatus = SOSCCThisDeviceIsInCircle(&circleError);
1068 BOOL xpcError = isErrorFromXPC(circleError);
1069 if(xpcError && currentCircleStatus == kSOSCCError) {
1070 secnotice("cjr", "returning early due to error returned from securityd: %@", circleError);
1073 if (kSOSCCInCircle != currentCircleStatus) {
1074 secnotice("cjr", "Left circle (%d), not handling remaining %lu applicants", currentCircleStatus, (unsigned long)newIds.count);
1075 cancelCurrentAlert(true);
1080 secnotice("cjr", "needsUpdate false, not updating alert");
1082 // Log circleStatusError?
1083 CFReleaseNull(circleStatusError);
1085 secnotice("cjr", "ACC token %d, status %d", notify_token, notify_register_status);
1086 debugState = @"processEvents F2";
1088 if (applicants.count == 0) {
1089 secnotice("cjr", "No applicants");
1091 debugState = @"processEvents F3";
1093 debugState = @"processEvents F4";
1095 debugState = @"processEvents F5";
1100 debugState = @"processEvents F6";
1101 notify_cancel(notify_token);
1102 debugState = @"processEvents DONE";
1108 int main (int argc, const char * argv[]) {
1109 os_transaction_t txion = os_transaction_create("com.apple.security.circle-join-requested");
1113 // NOTE: DISPATCH_QUEUE_PRIORITY_LOW will not actually manage to drain events in a lot of cases (like circleStatus != kSOSCCInCircle)
1114 xpc_set_event_stream_handler("com.apple.notifyd.matching", dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^(xpc_object_t object) {
1115 char *event_description = xpc_copy_description(object);
1116 const char *notificationName = xpc_dictionary_get_string(object, "Notification");
1118 if (notificationName && strcmp(notificationName, kUserKeybagStateChangeNotification)==0) {
1119 secnotice("cjr", "keybag changed!");
1120 keybagStateChange();
1123 secnotice("cjr", "notifyd event: %s\nAlert (%p) %s %s\ndebugState: %@", event_description, currentAlert,
1124 currentAlertIsForApplicants ? "for applicants" : "!applicants",
1125 currentAlertIsForKickOut ? "KO" : "!KO", debugState);
1126 free(event_description);
1129 xpc_activity_register(kLaunchLaterXPCName, XPC_ACTIVITY_CHECK_IN, ^(xpc_activity_t activity) {
1132 int falseInARow = 0;
1133 while (falseInARow < 2 && !_executeProcessEventsOnce) {
1134 if (processEvents()) {
1135 secnotice("cjr", "Processed events!!!");
1140 cancelCurrentAlert(false);
1141 if (doOnceInMainBlockChain) {
1142 doOnceInMainBlockChain();
1143 doOnceInMainBlockChain = NULL;
1148 secnotice("cjr", "Done");
1149 (void) txion; // But we really do want this around, compiler...