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];
148 secnotice("followup", "Posting a follow up (for SOS) of type repair");
149 [cdpd postFollowUpWithContext:context error:&localError ];
150 secnotice("cjr", "account is icdp");
152 secnotice("cjr", "request to CoreCDP to follow up failed: %@", localError);
155 secnotice("cjr", "CoreCDP handling follow up");
156 _hasPostedFollowupAndStillInError = true;
159 else if(_isAccountICDP && circleStatus == kSOSCCInCircle){
160 _hasPostedFollowupAndStillInError = false;
163 secnotice("cjr", "account not icdp");
167 static bool updateIsLocked ()
169 CFErrorRef aksError = NULL;
170 if (!SecAKSGetIsLocked(&_isLocked, &aksError)) {
172 secerror("Got error querying lock state: %@", aksError);
173 CFReleaseSafe(aksError);
177 _unlockedSinceBoot = YES;
181 static void keybagStateChange ()
183 secerror("osactivity initiated");
184 os_activity_initiate("keybagStateChanged", OS_ACTIVITY_FLAG_DEFAULT, ^{
185 BOOL wasLocked = _isLocked;
186 if ( updateIsLocked()) {
187 if (wasLocked == _isLocked)
188 secerror("still %s ignoring", _isLocked ? "locked" : "unlocked");
197 static void doOnceInMain(dispatch_block_t block)
199 if (doOnceInMainBlockChain) {
200 doOnceInMainBlockChain = ^{
201 doOnceInMainBlockChain();
205 doOnceInMainBlockChain = block;
210 static NSString *appleIDAccountName()
212 ACAccountStore *accountStore = [[ACAccountStore alloc] init];
213 ACAccount *primaryAppleAccount = [accountStore aa_primaryAppleAccount];
214 return primaryAppleAccount.username;
218 static CFOptionFlags flagsForAsk(Applicant *applicant)
220 return kCFUserNotificationPlainAlertLevel | CFUserNotificationSecureTextField(0);
224 // NOTE: gives precedence to OnScreen
225 static Applicant *firstApplicantWaitingOrOnScreen()
227 Applicant *waiting = nil;
228 for (Applicant *applicant in [applicants objectEnumerator]) {
229 if (applicant.applicantUIState == ApplicantOnScreen) {
231 } else if (applicant.applicantUIState == ApplicantWaiting) {
240 static NSMutableArray *applicantsInState(ApplicantUIState state)
242 NSMutableArray *results = [NSMutableArray new];
243 for (Applicant *applicant in [applicants objectEnumerator]) {
244 if (applicant.applicantUIState == state) {
245 [results addObject:applicant];
253 BOOL processRequests(CFErrorRef *error) {
254 NSMutableArray *toAccept = [[applicantsInState(ApplicantAccepted) mapWithBlock:^id(id obj) {return (id)[obj rawPeerInfo];}] mutableCopy];
255 NSMutableArray *toReject = [[applicantsInState(ApplicantRejected) mapWithBlock:^id(id obj) {return (id)[obj rawPeerInfo];}] mutableCopy];
258 if ([toAccept count]) {
259 secnotice("cjr", "Process accept: %@", toAccept);
260 ok = ok && SOSCCAcceptApplicants((__bridge CFArrayRef) toAccept, error);
262 secnotice("cjr", "kSOSCCHoldLockForInitialSync");
263 notify_post(kSOSCCHoldLockForInitialSync);
267 if ([toReject count]) {
268 secnotice("cjr", "Process reject: %@", toReject);
269 ok = ok && SOSCCRejectApplicants((__bridge CFArrayRef) toReject, error);
276 static void cancelCurrentAlert(bool stopRunLoop) {
277 if (currentAlertSource) {
278 CFRunLoopRemoveSource(CFRunLoopGetCurrent(), currentAlertSource, kCFRunLoopDefaultMode);
279 CFReleaseNull(currentAlertSource);
282 CFUserNotificationCancel(currentAlert);
283 CFReleaseNull(currentAlert);
286 CFRunLoopStop(CFRunLoopGetCurrent());
288 currentAlertIsForKickOut = currentAlertIsForApplicants = false;
292 static void askAboutAll(bool passwordFailure);
295 static void applicantChoice(CFUserNotificationRef userNotification, CFOptionFlags responseFlags)
297 ApplicantUIState choice;
299 if (kCFUserNotificationAlternateResponse == responseFlags) {
300 choice = ApplicantRejected;
301 } else if (kCFUserNotificationDefaultResponse == responseFlags) {
302 choice = ApplicantAccepted;
304 secnotice("cjr", "Unexpected response %lu", responseFlags);
305 choice = ApplicantRejected;
309 CFErrorRef error = NULL;
310 NSArray *onScreen = applicantsInState(ApplicantOnScreen);
312 [onScreen enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
313 Applicant* applicant = (Applicant *) obj;
314 applicant.applicantUIState = choice;
317 if (choice == ApplicantRejected) {
318 // If this device has ever set up the public key this should work without the password...
319 processed = processRequests(&error);
321 secnotice("cjr", "Didn't need password to process %@", onScreen);
322 cancelCurrentAlert(true);
325 // ...however if the public key gets lost we should "just" fall through to the validate
327 secnotice("cjr", "Couldn't process reject without password (e=%@) for %@ (will try with password next)", error, onScreen);
329 if(CFErrorIsMalfunctioningKeybagError(error)){
330 secnotice("cjr", "system is locked, dismiss the notification");
331 processApplicantsAfterUnlock = true;
335 CFReleaseNull(error);
338 NSString *password = (__bridge NSString *) CFUserNotificationGetResponseValue(userNotification, kCFUserNotificationTextFieldValuesKey, 0);
340 secnotice("cjr", "No password given, retry");
344 const char *passwordUTF8 = [password UTF8String];
345 NSData *passwordBytes = [NSData dataWithBytes:passwordUTF8 length:strlen(passwordUTF8)];
347 // Sometimes securityd crashes between SOSCCRegisterUserCredentials and processRequests
348 // (which results in a process error -- I think this is 13355140), as a workaround we retry
349 // failure a few times before we give up.
350 for (int try = 0; try < 5 && !processed; try++) {
351 if (!SOSCCTryUserCredentials(CFSTR(""), (__bridge CFDataRef) passwordBytes, &error)) {
352 secnotice("cjr", "Try user credentials failed %@", error);
353 if ((error == NULL) ||
354 (CFEqual(kSOSErrorDomain, CFErrorGetDomain(error)) && kSOSErrorWrongPassword == CFErrorGetCode(error))) {
355 secnotice("cjr", "Calling askAboutAll again...");
356 [onScreen enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
357 Applicant *applicant = (Applicant*) obj;
358 applicant.applicantUIState = ApplicantWaiting;
361 CFReleaseNull(error);
364 EXIT_LOGGED_FAILURE(EX_DATAERR);
367 processed = processRequests(&error);
369 secnotice("cjr", "Can't processRequests: %@ for %@", error, onScreen);
371 CFReleaseNull(error);
374 if (processed && firstApplicantWaitingOrOnScreen()) {
375 cancelCurrentAlert(false);
378 cancelCurrentAlert(true);
383 static void passwordFailurePrompt()
385 #pragma clang diagnostic push
386 #pragma clang diagnostic ignored "-Wformat-nonliteral"
387 NSString *pwIncorrect = [NSString stringWithFormat:(NSString *)CFBridgingRelease(SecCopyCKString(SEC_CK_PASSWORD_INCORRECT)), appleIDAccountName()];
388 #pragma clang diagnostic pop
389 NSString *tryAgain = CFBridgingRelease(SecCopyCKString(SEC_CK_TRY_AGAIN));
390 NSDictionary *noteAttributes = @{
391 (id) kCFUserNotificationAlertHeaderKey : pwIncorrect,
392 (id) kCFUserNotificationDefaultButtonTitleKey : tryAgain,
393 (id) kCFUserNotificationAlertTopMostKey : @YES, // get us onto the lock screen
394 (__bridge id) SBUserNotificationDontDismissOnUnlock: @YES,
395 (__bridge id) SBUserNotificationDismissOnLock : @NO,
397 CFOptionFlags flags = kCFUserNotificationPlainAlertLevel;
399 CFUserNotificationRef note = CFUserNotificationCreate(NULL, 0.0, flags, &err, (__bridge CFDictionaryRef) noteAttributes);
402 CFUserNotificationReceiveResponse(note, 0.0, &flags);
408 static NSString *getLocalizedApprovalBody(NSString *deviceType) {
409 CFStringRef applicationReminder = NULL;
411 if ([deviceType isEqualToString:@"iPhone"])
412 applicationReminder = SecCopyCKString(SEC_CK_APPROVAL_BODY_IOS_IPHONE);
413 else if ([deviceType isEqualToString:@"iPod"])
414 applicationReminder = SecCopyCKString(SEC_CK_APPROVAL_BODY_IOS_IPOD);
415 else if ([deviceType isEqualToString:@"iPad"])
416 applicationReminder = SecCopyCKString(SEC_CK_APPROVAL_BODY_IOS_IPAD);
417 else if ([deviceType isEqualToString:@"Mac"])
418 applicationReminder = SecCopyCKString(SEC_CK_APPROVAL_BODY_IOS_MAC);
420 applicationReminder = SecCopyCKString(SEC_CK_APPROVAL_BODY_IOS_GENERIC);
422 return (__bridge_transfer NSString *) applicationReminder;
426 static NSDictionary *createNote(Applicant *applicantToAskAbout)
428 if(!applicantToAskAbout || !applicantToAskAbout.name || !applicantToAskAbout.deviceType)
431 #pragma clang diagnostic push
432 #pragma clang diagnostic ignored "-Wformat-nonliteral"
433 NSString *header = [NSString stringWithFormat: (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_APPROVAL_TITLE), applicantToAskAbout.name];
434 NSString *body = [NSString stringWithFormat: getLocalizedApprovalBody(applicantToAskAbout.deviceType), appleIDAccountName()];
435 #pragma clang diagnostic pop
438 (id) kCFUserNotificationAlertHeaderKey : header,
439 (id) kCFUserNotificationAlertMessageKey : body,
440 (id) kCFUserNotificationDefaultButtonTitleKey : (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_APPROVE),
441 (id) kCFUserNotificationAlternateButtonTitleKey: (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_DECLINE),
442 (id) kCFUserNotificationTextFieldTitlesKey : (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_ICLOUD_PASSWORD),
443 (id) kCFUserNotificationAlertTopMostKey : @YES, // get us onto the lock screen
444 (__bridge_transfer id) SBUserNotificationDontDismissOnUnlock: @YES,
445 (__bridge_transfer id) SBUserNotificationDismissOnLock : @NO,
450 static void askAboutAll(bool passwordFailure)
452 if ([[MCProfileConnection sharedConnection] effectiveBoolValueForSetting: MCFeatureAccountModificationAllowed] == MCRestrictedBoolExplicitNo) {
453 secnotice("cjr", "Account modifications not allowed.");
457 if (passwordFailure) {
458 passwordFailurePrompt();
461 if ((passwordFailure || !currentAlertIsForApplicants) && currentAlert) {
462 if (!currentAlertIsForApplicants) {
463 CFUserNotificationCancel(currentAlert);
465 // after password failure we need to remove the existing alert and supporting objects
466 // because we can't reuse them.
467 CFReleaseNull(currentAlert);
468 CFReleaseNull(currentAlertSource);
470 currentAlertIsForApplicants = true;
472 Applicant *applicantToAskAbout = firstApplicantWaitingOrOnScreen();
473 secnotice("cjr", "Asking about: %@ (of: %@)", applicantToAskAbout, applicants);
475 NSDictionary *noteAttributes = createNote(applicantToAskAbout);
476 if(!noteAttributes) {
477 secnotice("cjr", "NULL data for %@", applicantToAskAbout);
478 cancelCurrentAlert(true);
482 CFOptionFlags flags = flagsForAsk(applicantToAskAbout);
485 SInt32 err = CFUserNotificationUpdate(currentAlert, 0, flags, (__bridge CFDictionaryRef) noteAttributes);
487 secnotice("cjr", "CFUserNotificationUpdate err=%d", (int)err);
488 EXIT_LOGGED_FAILURE(EX_SOFTWARE);
492 currentAlert = CFUserNotificationCreate(NULL, 0.0, flags, &err, (__bridge CFDictionaryRef) noteAttributes);
494 secnotice("cjr", "Can't make notification for %@ err=%x", applicantToAskAbout, (int)err);
495 EXIT_LOGGED_FAILURE(EX_SOFTWARE);
498 currentAlertSource = CFUserNotificationCreateRunLoopSource(NULL, currentAlert, applicantChoice, 0);
499 CFRunLoopAddSource(CFRunLoopGetCurrent(), currentAlertSource, kCFRunLoopDefaultMode);
502 applicantToAskAbout.applicantUIState = ApplicantOnScreen;
506 static void scheduleActivity(int alertInterval)
508 xpc_object_t options = xpc_dictionary_create(NULL, NULL, 0);
509 xpc_dictionary_set_uint64(options, XPC_ACTIVITY_DELAY, alertInterval);
510 xpc_dictionary_set_uint64(options, XPC_ACTIVITY_GRACE_PERIOD, XPC_ACTIVITY_INTERVAL_1_MIN);
511 xpc_dictionary_set_bool(options, XPC_ACTIVITY_REPEATING, false);
512 xpc_dictionary_set_bool(options, XPC_ACTIVITY_ALLOW_BATTERY, true);
513 xpc_dictionary_set_string(options, XPC_ACTIVITY_PRIORITY, XPC_ACTIVITY_PRIORITY_UTILITY);
515 xpc_activity_register(kLaunchLaterXPCName, options, ^(xpc_activity_t activity) {
516 secnotice("cjr", "activity handler fired");
521 static void reminderChoice(CFUserNotificationRef userNotification, CFOptionFlags responseFlags) {
522 if (responseFlags == kCFUserNotificationAlternateResponse || responseFlags == kCFUserNotificationDefaultResponse) {
523 PersistentState *state = [PersistentState loadFromStorage];
524 NSDate *nowish = [NSDate new];
525 state.pendingApplicationReminder = [nowish dateByAddingTimeInterval: state.pendingApplicationReminderAlertInterval];
526 scheduleActivity(state.pendingApplicationReminderAlertInterval);
527 [state writeToStorage];
528 if (responseFlags == kCFUserNotificationAlternateResponse) {
530 BOOL ok = [[LSApplicationWorkspace defaultWorkspace] openSensitiveURL:[NSURL URLWithString:castleKeychainUrl] withOptions:nil];
531 secnotice("cjr", "%s iCSC: opening %@ ok=%d", __FUNCTION__, castleKeychainUrl, ok);
535 cancelCurrentAlert(true);
539 static bool iCloudResetAvailable() {
540 SecureBackup *backupd = [SecureBackup new];
541 NSDictionary *backupdResults;
542 NSError *error = [backupd getAccountInfoWithInfo:nil results:&backupdResults];
543 secnotice("cjr", "SecureBackup e=%@ r=%@", error, backupdResults);
544 return (error == nil && [backupdResults[kSecureBackupIsEnabledKey] isEqualToNumber:@YES]);
548 static NSString *getLocalizedApplicationReminder() {
549 CFStringRef applicationReminder = NULL;
550 switch (MGGetSInt32Answer(kMGQDeviceClassNumber, MGDeviceClassInvalid)) {
551 case MGDeviceClassiPhone:
552 applicationReminder = SecCopyCKString(SEC_CK_REMINDER_BODY_IOS_IPHONE);
554 case MGDeviceClassiPod:
555 applicationReminder = SecCopyCKString(SEC_CK_REMINDER_BODY_IOS_IPOD);
557 case MGDeviceClassiPad:
558 applicationReminder = SecCopyCKString(SEC_CK_REMINDER_BODY_IOS_IPAD);
561 applicationReminder = SecCopyCKString(SEC_CK_REMINDER_BODY_IOS_GENERIC);
564 return (__bridge_transfer NSString *) applicationReminder;
567 static bool isSOSInternalDevice(void) {
568 static dispatch_once_t onceToken;
569 static BOOL internal = NO;
570 dispatch_once(&onceToken, ^{
571 internal = os_variant_has_internal_diagnostics("com.apple.security");
576 static void postApplicationReminderAlert(NSDate *nowish, PersistentState *state, unsigned int alertInterval)
578 NSString *body = getLocalizedApplicationReminder();
579 bool has_iCSC = iCloudResetAvailable();
581 if (isSOSInternalDevice() &&
582 state.defaultPendingApplicationReminderAlertInterval != state.pendingApplicationReminderAlertInterval) {
584 body = [body stringByAppendingFormat: @"〖debug interval %u; wait time %@〗",
585 state.pendingApplicationReminderAlertInterval,
586 [nowish copyDescriptionOfIntervalSince:state.applicationDate]];
590 NSDictionary *pendingAttributes = @{
591 (id) kCFUserNotificationAlertHeaderKey : CFBridgingRelease(SecCopyCKString(SEC_CK_REMINDER_TITLE_IOS)),
592 (id) kCFUserNotificationAlertMessageKey : body,
593 (id) kCFUserNotificationDefaultButtonTitleKey : CFBridgingRelease(SecCopyCKString(SEC_CK_REMINDER_BUTTON_OK)),
594 (id) kCFUserNotificationAlternateButtonTitleKey: has_iCSC ? CFBridgingRelease(SecCopyCKString(SEC_CK_REMINDER_BUTTON_ICSC)) : @"",
595 (id) kCFUserNotificationAlertTopMostKey : @YES,
596 (__bridge id) SBUserNotificationDontDismissOnUnlock : @YES,
597 (__bridge id) SBUserNotificationDismissOnLock : @NO,
600 currentAlert = CFUserNotificationCreate(NULL, 0.0, kCFUserNotificationPlainAlertLevel, &err, (__bridge CFDictionaryRef) pendingAttributes);
603 secnotice("cjr", "Can't make pending notification err=%x", (int)err);
605 currentAlertIsForApplicants = false;
606 currentAlertSource = CFUserNotificationCreateRunLoopSource(NULL, currentAlert, reminderChoice, 0);
607 CFRunLoopAddSource(CFRunLoopGetCurrent(), currentAlertSource, kCFRunLoopDefaultMode);
612 static void kickOutChoice(CFUserNotificationRef userNotification, CFOptionFlags responseFlags) {
613 secnotice("cjr", "kOC %@ %lu", userNotification, responseFlags);
615 //default response: continue -> settings pref pane advanced keychain sync page
616 if (responseFlags == kCFUserNotificationDefaultResponse) {
617 // We need to let things unwind to main for the new state to get saved
619 NSURL *url = [NSURL URLWithString: _isAccountICDP ? rejoinICDPUrl : castleKeychainUrl];
620 BOOL ok = [[LSApplicationWorkspace defaultWorkspace] openSensitiveURL:url withOptions:nil];
621 secnotice("cjr","kickOutChoice account is iCDP: %d", _isAccountICDP);
622 secnotice("cjr", "ok=%d opening %@", ok, url);
625 //alternate response: later -> call CD
626 else if (responseFlags == kCFUserNotificationAlternateResponse) {
627 // We need to let things unwind to main for the new state to get saved
630 CDPFollowUpController *cdpd = [[CDPFollowUpController alloc] init];
631 NSError *localError = nil;
632 CDPFollowUpContext *context = [CDPFollowUpContext contextForStateRepair];
634 secnotice("followup", "Posting a follow up (for SOS) of type repair");
635 [cdpd postFollowUpWithContext:context error:&localError ];
637 secnotice("cjr", "request to CoreCDP to follow up failed: %@", localError);
640 secnotice("cjr", "CoreCDP handling follow up");
641 _hasPostedFollowupAndStillInError = true;
647 cancelCurrentAlert(true);
651 CFStringRef const CJRAggdDepartureReasonKey = CFSTR("com.apple.security.circlejoinrequested.departurereason");
652 CFStringRef const CJRAggdNumCircleDevicesKey = CFSTR("com.apple.security.circlejoinrequested.numcircledevices");
654 static void postKickedOutAlert(enum DepartureReason reason)
656 NSString *header = nil;
657 NSString *message = nil;
659 // <rdar://problem/20358568> iCDP telemetry: ☂ Statistics on circle reset and drop out events
660 ADClientSetValueForScalarKey(CJRAggdDepartureReasonKey, reason);
662 int64_t num_peers = 0;
663 CFArrayRef peerList = SOSCCCopyPeerPeerInfo(NULL);
665 num_peers = CFArrayGetCount(peerList);
666 if (num_peers > 99) {
667 // Round down # peers to 2 significant digits
669 for (factor = 10; num_peers >= 100*factor; factor *= 10) ;
670 num_peers = (num_peers / factor) * factor;
674 ADClientSetValueForScalarKey(CJRAggdNumCircleDevicesKey, num_peers);
676 debugState = @"pKOA A";
677 secnotice("cjr", "DepartureReason %d", reason);
679 case kSOSDiscoveredRetirement:
680 case kSOSLostPrivateKey:
681 case kSOSWithdrewMembership:
682 // Was: SEC_CK_CR_BODY_WITHDREW
683 // "... if you turn off a switch you have some idea why the light is off" - Murf
686 case kSOSNeverAppliedToCircle:
687 // We didn't get kicked out, we were never here. This should only happen if we changed iCloud accounts
688 // (and we had sync on in the previous one, and never had it on in the new one). As this is explicit
689 // user action alot of the "Light switch" argument (above) applies.
692 case kSOSPasswordChanged:
693 case kSOSNeverLeftCircle:
694 case kSOSMembershipRevoked:
695 case kSOSLeftUntrustedCircle:
697 header = (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_PWD_REQUIRED_TITLE);
698 message = (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_PWD_REQUIRED_BODY_IOS);
702 if (isSOSInternalDevice()) {
703 static const char *departureReasonStrings[] = {
704 "kSOSDepartureReasonError",
705 "kSOSNeverLeftCircle",
706 "kSOSWithdrewMembership",
707 "kSOSMembershipRevoked",
708 "kSOSLeftUntrustedCircle",
709 "kSOSNeverAppliedToCircle",
710 "kSOSDiscoveredRetirement",
711 "kSOSLostPrivateKey",
712 "kSOSPasswordChanged",
715 int idx = (kSOSDepartureReasonError <= reason && reason <= kSOSLostPrivateKey) ? reason : (kSOSLostPrivateKey + 1);
716 #pragma clang diagnostic push
717 #pragma clang diagnostic ignored "-Wformat-nonliteral"
718 NSString *reason_str = [NSString stringWithFormat:(__bridge_transfer NSString *) SecCopyCKString(SEC_CK_CR_REASON_INTERNAL),
719 departureReasonStrings[idx]];
720 #pragma clang diagnostic pop
721 message = [message stringByAppendingString: reason_str];
724 NSDictionary *kickedAttributes = @{
725 (id) kCFUserNotificationAlertHeaderKey : header,
726 (id) kCFUserNotificationAlertMessageKey : message,
727 (id) kCFUserNotificationDefaultButtonTitleKey : (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_CONTINUE),
728 (id) kCFUserNotificationAlternateButtonTitleKey: (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_NOT_NOW),
729 (id) kCFUserNotificationAlertTopMostKey : @YES,
730 (__bridge id) SBUserNotificationDismissOnLock : @NO,
731 (__bridge id) SBUserNotificationDontDismissOnUnlock : @YES,
735 if (currentAlertIsForKickOut) {
736 debugState = @"pKOA B";
737 secnotice("cjr", "Updating existing alert %@ with %@", currentAlert, kickedAttributes);
738 CFUserNotificationUpdate(currentAlert, 0, kCFUserNotificationPlainAlertLevel, (__bridge CFDictionaryRef) kickedAttributes);
740 debugState = @"pKOA C";
742 CFUserNotificationRef note = CFUserNotificationCreate(NULL, 0.0, kCFUserNotificationPlainAlertLevel, &err, (__bridge CFDictionaryRef) kickedAttributes);
743 assert((note == NULL) == (err != 0));
745 secnotice("cjr", "Can't make kicked out notification err=%x", (int)err);
748 currentAlertIsForApplicants = false;
749 currentAlertIsForKickOut = true;
752 secnotice("cjr", "New ko alert %@ a=%@", currentAlert, kickedAttributes);
753 currentAlertSource = CFUserNotificationCreateRunLoopSource(NULL, currentAlert, kickOutChoice, 0);
754 CFRunLoopAddSource(CFRunLoopGetCurrent(), currentAlertSource, kCFRunLoopDefaultMode);
755 int backupStateChangeToken;
756 notify_register_dispatch("com.apple.EscrowSecurityAlert.reset", &backupStateChangeToken, dispatch_get_main_queue(), ^(int token) {
757 if (currentAlert == note) {
758 secnotice("cjr", "Backup state might have changed (dS=%@)", debugState);
759 postKickedOutAlert(reason);
761 secnotice("cjr", "Backup state may have changed, but we don't care anymore (dS=%@)", debugState);
765 debugState = @"pKOA D";
767 debugState = @"pKOA E";
768 notify_cancel(backupStateChangeToken);
771 debugState = @"pKOA Z";
774 static void askForCDPFollowup() {
776 NSError *localError = nil;
777 CDPFollowUpController *cdpd = [[CDPFollowUpController alloc] init];
778 CDPFollowUpContext *context = [CDPFollowUpContext contextForStateRepair];
780 secnotice("followup", "Posting a follow up (for SOS) of type repair");
781 [cdpd postFollowUpWithContext:context error:&localError ];
783 secnotice("cjr", "request to CoreCDP to follow up failed: %@", localError);
786 secnotice("cjr", "CoreCDP handling follow up");
787 _hasPostedFollowupAndStillInError = true;
792 static bool processEvents()
794 debugState = @"processEvents A";
796 CFErrorRef error = NULL;
797 CFErrorRef departError = NULL;
798 SOSCCStatus circleStatus = SOSCCThisDeviceIsInCircleNonCached(&error);
799 enum DepartureReason departureReason = SOSCCGetLastDepartureReason(&departError);
801 BOOL abortFromError = isErrorFromXPC(error);
802 if(abortFromError && circleStatus == kSOSCCError) {
803 secnotice("cjr", "returning from processEvents due to error returned from securityd: %@", error);
806 if (departureReason == kSOSDepartureReasonError && departError && (CFEqual(sSecXPCErrorDomain, CFErrorGetDomain(departError)))) {
807 secnotice("cjr", "XPC error while checking last departure reason: \"%@\", not processing events", departError);
811 NSDate *nowish = [NSDate date];
812 PersistentState *state = [PersistentState loadFromStorage];
813 secnotice("cjr", "CircleStatus %d -> %d{%d} (s=%p)", state.lastCircleStatus, circleStatus, departureReason, state);
815 // Pending application reminder
816 NSTimeInterval timeUntilApplicationAlert = [state.pendingApplicationReminder timeIntervalSinceDate:nowish];
817 secnotice("cjr", "Time until pendingApplicationReminder (%@) %f", [state.pendingApplicationReminder debugDescription], timeUntilApplicationAlert);
818 if (circleStatus == kSOSCCRequestPending) {
819 if (timeUntilApplicationAlert <= 0) {
820 debugState = @"reminderAlert";
821 postApplicationReminderAlert(nowish, state, state.pendingApplicationReminderAlertInterval);
823 scheduleActivity(ceil(timeUntilApplicationAlert));
827 PSKeychainSyncIsUsingICDP();
829 // Refresh because sometimes we're fixed elsewhere before we get here.
830 CFReleaseNull(error);
831 circleStatus = SOSCCThisDeviceIsInCircleNonCached(&error);
832 abortFromError = isErrorFromXPC(error);
833 if(abortFromError && circleStatus == kSOSCCError) {
834 secnotice("cjr", "returning from processEvents due to error returned from securityd: %@", error);
839 state.lastCircleStatus = circleStatus;
840 [state writeToStorage];
841 if(_hasPostedFollowupAndStillInError == true) {
842 secnotice("cjr", "followup not resolved");
843 _executeProcessEventsOnce = true;
847 switch(circleStatus) {
849 secnotice("cjr", "follow up should be resolved");
850 _executeProcessEventsOnce = true;
851 _hasPostedFollowupAndStillInError = false;
854 secnotice("cjr", "error from SOSCCThisDeviceIsInCircle: %@", error);
856 _executeProcessEventsOnce = true;
858 case kSOSCCCircleAbsent:
859 case kSOSCCNotInCircle:
861 You would think we could count on not being iCDP if the account was signed out. Evidently that's wrong.
862 So we'll go based on the artifact that when the account object is reset (like by signing out) the
863 departureReason will be set to kSOSDepartureReasonError. So we won't push to get back into a circle if that's
864 the current reason. I've checked code for other ways we could be out. If we boot and can't load the account
865 we'll end up with kSOSDepartureReasonError. Then too if we end up in kSOSDepartureReasonError and reboot we end up
866 in the same place. Leave it to cdpd to decide whether the user needs to sign in to an account.
868 if(departureReason != kSOSDepartureReasonError) {
869 secnotice("cjr", "iCDP: We need to get back into the circle");
872 secnotice("cjr", "iCDP: We appear to not be associated with an iCloud account");
874 _executeProcessEventsOnce = true;
876 case kSOSCCRequestPending:
879 secnotice("cjr", "Unknown circle status %d", circleStatus);
882 } else if(circleStatus == kSOSCCError && state.lastCircleStatus != kSOSCCError && (departureReason == kSOSNeverLeftCircle)) {
883 secnotice("cjr", "SA: error from SOSCCThisDeviceIsInCircle: %@", error);
884 CFIndex errorCode = CFErrorGetCode(error);
885 if(errorCode == kSOSErrorPublicKeyAbsent){
886 secnotice("cjr", "SA: We need the password to re-validate ourselves - it's changed on another device");
887 postKickedOutAlert(kSOSPasswordChanged);
888 state.lastCircleStatus = kSOSCCError;
889 [state writeToStorage];
893 // No longer in circle?
894 if ((state.lastCircleStatus == kSOSCCInCircle && (circleStatus == kSOSCCNotInCircle || circleStatus == kSOSCCCircleAbsent)) ||
895 (state.lastCircleStatus == kSOSCCCircleAbsent && circleStatus == kSOSCCNotInCircle && state.absentCircleWithNoReason) ||
896 state.debugShowLeftReason) {
897 // Used to be in the circle, now we aren't - tell the user why
898 debugState = @"processEvents B";
900 if (state.debugShowLeftReason) {
901 secnotice("cjr", "debugShowLeftReason: %@", state.debugShowLeftReason);
902 departureReason = [state.debugShowLeftReason intValue];
903 state.debugShowLeftReason = nil;
904 CFReleaseNull(departError);
905 [state writeToStorage];
908 if (departureReason != kSOSDepartureReasonError) {
909 state.absentCircleWithNoReason = (circleStatus == kSOSCCCircleAbsent && departureReason == kSOSNeverLeftCircle);
910 secnotice("cjr", "Depature reason %d", departureReason);
912 secnotice("cjr", "posting revocation notification!");
913 postKickedOutAlert(departureReason);
915 else if(_isAccountICDP && _hasPostedFollowupAndStillInError == false){
916 NSError *localError = nil;
917 CDPFollowUpController *cdpd = [[CDPFollowUpController alloc] init];
918 CDPFollowUpContext *context = [CDPFollowUpContext contextForStateRepair];
920 secnotice("followup", "Posting a follow up (for SOS) of type repair");
921 [cdpd postFollowUpWithContext:context error:&localError ];
923 secnotice("cjr", "request to CoreCDP to follow up failed: %@", localError);
926 secnotice("cjr", "CoreCDP handling follow up");
927 _hasPostedFollowupAndStillInError = true;
931 secnotice("cjr", "still waiting for followup to resolve");
933 secnotice("cjr", "pKOA returned (cS %d lCS %d)", circleStatus, state.lastCircleStatus);
935 secnotice("cjr", "Couldn't get last departure reason: %@", departError);
940 // Circle applications: pending request(s) started / completed
941 debugState = @"processEvents C";
942 if (circleStatus != state.lastCircleStatus) {
943 SOSCCStatus lastCircleStatus = state.lastCircleStatus;
944 state.lastCircleStatus = circleStatus;
946 if (lastCircleStatus != kSOSCCRequestPending && circleStatus == kSOSCCRequestPending) {
947 secnotice("cjr", "Pending request started");
948 state.applicationDate = nowish;
949 state.pendingApplicationReminder = [nowish dateByAddingTimeInterval: state.pendingApplicationReminderAlertInterval];
950 scheduleActivity(state.pendingApplicationReminderAlertInterval);
952 if (lastCircleStatus == kSOSCCRequestPending && circleStatus != kSOSCCRequestPending) {
953 secnotice("cjr", "Pending request completed");
954 state.applicationDate = [NSDate distantPast];
955 state.pendingApplicationReminder = [NSDate distantFuture];
958 [state writeToStorage];
961 if (circleStatus != kSOSCCInCircle) {
962 if (circleStatus == kSOSCCRequestPending && currentAlert) {
964 CFUserNotificationRef postedAlert = currentAlert;
966 debugState = @"processEvents D1";
967 notify_register_dispatch(kSOSCCCircleChangedNotification, ¬ifyToken, dispatch_get_main_queue(), ^(int token) {
968 if (postedAlert != currentAlert) {
969 secnotice("cjr", "-- CC after original alert gone (currentAlertIsForApplicants %d, pA %p, cA %p -- %@)",
970 currentAlertIsForApplicants, postedAlert, currentAlert, currentAlert);
971 notify_cancel(token);
973 CFErrorRef localError = NULL;
974 SOSCCStatus newCircleStatus = SOSCCThisDeviceIsInCircle(&localError);
975 BOOL xpcError = isErrorFromXPC(localError);
976 if(xpcError && newCircleStatus == kSOSCCError) {
977 secnotice("cjr", "returning from processEvents due to error returned from securityd: %@", localError);
981 if (newCircleStatus != kSOSCCRequestPending) {
982 if (newCircleStatus == kSOSCCError)
983 secnotice("cjr", "No longer pending (nCS=%d, alert=%@) error: %@", newCircleStatus, currentAlert, localError);
985 secnotice("cjr", "No longer pending (nCS=%d, alert=%@)", newCircleStatus, currentAlert);
986 cancelCurrentAlert(true);
988 secnotice("cjr", "Still pending...");
990 CFReleaseNull(localError);
993 debugState = @"processEvents D2";
994 secnotice("cjr", "NOTE: currentAlertIsForApplicants %d, token %d", currentAlertIsForApplicants, notifyToken);
998 debugState = @"processEvents D4";
999 secnotice("cjr", "SOSCCThisDeviceIsInCircle status %d, not checking applicants", circleStatus);
1005 debugState = @"processEvents E";
1006 applicants = [NSMutableDictionary new];
1007 for (id applicantInfo in (__bridge_transfer NSArray *) SOSCCCopyApplicantPeerInfo(&error)) {
1008 Applicant *applicant = [[Applicant alloc] initWithPeerInfo:(__bridge SOSPeerInfoRef) applicantInfo];
1009 applicants[applicant.idString] = applicant;
1012 // Log error from SOSCCCopyApplicantPeerInfo() above?
1013 CFReleaseNull(error);
1015 int notify_token = -42;
1016 debugState = @"processEvents F";
1017 int notify_register_status = notify_register_dispatch(kSOSCCCircleChangedNotification, ¬ify_token, dispatch_get_main_queue(), ^(int token) {
1018 secnotice("cjr", "Notified: %s", kSOSCCCircleChangedNotification);
1019 CFErrorRef circleStatusError = NULL;
1021 bool needsUpdate = false;
1022 CFErrorRef copyPeerError = NULL;
1023 NSMutableSet *newIds = [NSMutableSet new];
1024 for (id applicantInfo in (__bridge_transfer NSArray *) SOSCCCopyApplicantPeerInfo(©PeerError)) {
1025 Applicant *newApplicant = [[Applicant alloc] initWithPeerInfo:(__bridge SOSPeerInfoRef) applicantInfo];
1026 [newIds addObject:newApplicant.idString];
1027 Applicant *existingApplicant = applicants[newApplicant.idString];
1028 if (existingApplicant) {
1029 switch (existingApplicant.applicantUIState) {
1030 case ApplicantWaiting:
1031 applicants[newApplicant.idString] = newApplicant;
1034 case ApplicantOnScreen:
1035 newApplicant.applicantUIState = ApplicantOnScreen;
1036 applicants[newApplicant.idString] = newApplicant;
1040 secnotice("cjr", "Update to %@ >> %@ with pending order, should work out ok though", existingApplicant, newApplicant);
1045 applicants[newApplicant.idString] = newApplicant;
1048 if (copyPeerError) {
1049 secnotice("cjr", "Could not update peer info array: %@", copyPeerError);
1050 CFRelease(copyPeerError);
1054 NSMutableArray *idsToRemoveFromApplicants = [NSMutableArray new];
1055 for (NSString *exisitngId in [applicants keyEnumerator]) {
1056 if (![newIds containsObject:exisitngId]) {
1057 [idsToRemoveFromApplicants addObject:exisitngId];
1061 [applicants removeObjectsForKeys:idsToRemoveFromApplicants];
1063 if (newIds.count == 0) {
1064 secnotice("cjr", "All applicants were handled elsewhere");
1065 cancelCurrentAlert(true);
1067 CFErrorRef circleError = NULL;
1068 SOSCCStatus currentCircleStatus = SOSCCThisDeviceIsInCircle(&circleError);
1069 BOOL xpcError = isErrorFromXPC(circleError);
1070 if(xpcError && currentCircleStatus == kSOSCCError) {
1071 secnotice("cjr", "returning early due to error returned from securityd: %@", circleError);
1074 if (kSOSCCInCircle != currentCircleStatus) {
1075 secnotice("cjr", "Left circle (%d), not handling remaining %lu applicants", currentCircleStatus, (unsigned long)newIds.count);
1076 cancelCurrentAlert(true);
1081 secnotice("cjr", "needsUpdate false, not updating alert");
1083 // Log circleStatusError?
1084 CFReleaseNull(circleStatusError);
1086 secnotice("cjr", "ACC token %d, status %d", notify_token, notify_register_status);
1087 debugState = @"processEvents F2";
1089 if (applicants.count == 0) {
1090 secnotice("cjr", "No applicants");
1092 debugState = @"processEvents F3";
1094 debugState = @"processEvents F4";
1096 debugState = @"processEvents F5";
1101 debugState = @"processEvents F6";
1102 notify_cancel(notify_token);
1103 debugState = @"processEvents DONE";
1109 int main (int argc, const char * argv[]) {
1110 os_transaction_t txion = os_transaction_create("com.apple.security.circle-join-requested");
1114 // NOTE: DISPATCH_QUEUE_PRIORITY_LOW will not actually manage to drain events in a lot of cases (like circleStatus != kSOSCCInCircle)
1115 xpc_set_event_stream_handler("com.apple.notifyd.matching", dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^(xpc_object_t object) {
1116 char *event_description = xpc_copy_description(object);
1117 const char *notificationName = xpc_dictionary_get_string(object, "Notification");
1119 if (notificationName && strcmp(notificationName, kUserKeybagStateChangeNotification)==0) {
1120 secnotice("cjr", "keybag changed!");
1121 keybagStateChange();
1124 secnotice("cjr", "notifyd event: %s\nAlert (%p) %s %s\ndebugState: %@", event_description, currentAlert,
1125 currentAlertIsForApplicants ? "for applicants" : "!applicants",
1126 currentAlertIsForKickOut ? "KO" : "!KO", debugState);
1127 free(event_description);
1130 xpc_activity_register(kLaunchLaterXPCName, XPC_ACTIVITY_CHECK_IN, ^(xpc_activity_t activity) {
1133 int falseInARow = 0;
1134 while (falseInARow < 2 && !_executeProcessEventsOnce) {
1135 if (processEvents()) {
1136 secnotice("cjr", "Processed events!!!");
1141 cancelCurrentAlert(false);
1142 if (doOnceInMainBlockChain) {
1143 doOnceInMainBlockChain();
1144 doOnceInMainBlockChain = NULL;
1149 secnotice("cjr", "Done");
1150 (void) txion; // But we really do want this around, compiler...