2 // CircleJoinRequested.m
5 // Created by J Osborne on 3/5/13.
6 // Copyright (c) 2013-2014 Apple Inc. All Rights Reserved.
8 #import <Accounts/Accounts.h>
9 #import <Accounts/ACAccountStore_Private.h>
10 #import <Accounts/ACAccountType_Private.h>
11 #import <AggregateDictionary/ADClient.h>
12 #import <AppSupport/AppSupportUtils.h>
13 #import <AppleAccount/AppleAccount.h>
14 #import <AppleAccount/ACAccountStore+AppleAccount.h>
15 #import <CloudServices/SecureBackup.h>
16 #import <CoreFoundation/CFUserNotification.h>
17 #import <Foundation/Foundation.h>
18 #import <ManagedConfiguration/MCProfileConnection.h>
19 #import <ManagedConfiguration/MCFeatures.h>
20 #import <MobileCoreServices/MobileCoreServices.h>
21 #import <MobileCoreServices/LSApplicationWorkspace.h>
22 #import <MobileGestalt.h>
23 #import <ProtectedCloudStorage/CloudIdentity.h>
24 #import <Security/SecFrameworkStrings.h>
25 #import <SpringBoardServices/SBSCFUserNotificationKeys.h>
26 #include <dispatch/dispatch.h>
27 #include "SecureObjectSync/SOSCloudCircle.h"
28 #include "SecureObjectSync/SOSPeerInfo.h"
32 #import "NSArray+map.h"
33 #import "PersistentState.h"
34 #include <xpc/private.h>
36 #import "NSDate+TimeIntervalDescription.h"
37 #include <xpc/activity.h>
38 #include <xpc/private.h>
40 #include "utilities/SecCFRelease.h"
41 #include "utilities/debugging.h"
43 // As long as we are logging the failure use exit code of zero to make launchd happy
44 #define EXIT_LOGGED_FAILURE(code) xpc_transaction_end(); exit(0)
46 const char *kLaunchLaterXPCName = "com.apple.security.CircleJoinRequestedTick";
47 CFRunLoopSourceRef currentAlertSource = NULL;
48 CFUserNotificationRef currentAlert = NULL;
49 bool currentAlertIsForApplicants = true;
50 bool currentAlertIsForKickOut = false;
51 NSMutableDictionary *applicants = nil;
52 volatile NSString *debugState = @"main?";
53 dispatch_block_t doOnceInMainBlockChain = NULL;
55 NSString *castleKeychainUrl = @"prefs:root=CASTLE&path=Keychain/ADVANCED";
56 NSString *rejoinICDPUrl = @"prefs:root=CASTLE&aaaction=CDP&command=rejoin";
58 static void doOnceInMain(dispatch_block_t block)
60 if (doOnceInMainBlockChain) {
61 doOnceInMainBlockChain = ^{
62 doOnceInMainBlockChain();
66 doOnceInMainBlockChain = block;
71 static NSString *appleIDAccountName()
73 ACAccountStore *accountStore = [[ACAccountStore alloc] init];
74 ACAccount *primaryAppleAccount = [accountStore aa_primaryAppleAccount];
75 return primaryAppleAccount.username;
79 static CFOptionFlags flagsForAsk(Applicant *applicant)
81 return kCFUserNotificationPlainAlertLevel | CFUserNotificationSecureTextField(0);
85 // NOTE: gives precedence to OnScreen
86 static Applicant *firstApplicantWaitingOrOnScreen()
88 Applicant *waiting = nil;
89 for (Applicant *applicant in [applicants objectEnumerator]) {
90 if (applicant.applicantUIState == ApplicantOnScreen) {
92 } else if (applicant.applicantUIState == ApplicantWaiting) {
101 static NSMutableArray *applicantsInState(ApplicantUIState state)
103 NSMutableArray *results = [NSMutableArray new];
104 for (Applicant *applicant in [applicants objectEnumerator]) {
105 if (applicant.applicantUIState == state) {
106 [results addObject:applicant];
114 static BOOL processRequests(CFErrorRef *error) {
115 NSMutableArray *toAccept = [[applicantsInState(ApplicantAccepted) mapWithBlock:^id(id obj) {return (id)[obj rawPeerInfo];}] mutableCopy];
116 NSMutableArray *toReject = [[applicantsInState(ApplicantRejected) mapWithBlock:^id(id obj) {return (id)[obj rawPeerInfo];}] mutableCopy];
119 if ([toAccept count]) {
120 NSLog(@"Process accept: %@", toAccept);
121 ok = ok && SOSCCAcceptApplicants((__bridge CFArrayRef) toAccept, error);
123 NSLog(@"kSOSCCHoldLockForInitialSync");
124 notify_post(kSOSCCHoldLockForInitialSync);
128 if ([toReject count]) {
129 NSLog(@"Process reject: %@", toReject);
130 ok = ok && SOSCCRejectApplicants((__bridge CFArrayRef) toReject, error);
137 static void cancelCurrentAlert(bool stopRunLoop) {
138 if (currentAlertSource) {
139 CFRunLoopRemoveSource(CFRunLoopGetCurrent(), currentAlertSource, kCFRunLoopDefaultMode);
140 CFReleaseNull(currentAlertSource);
143 CFUserNotificationCancel(currentAlert);
144 CFReleaseNull(currentAlert);
147 CFRunLoopStop(CFRunLoopGetCurrent());
149 currentAlertIsForKickOut = currentAlertIsForApplicants = false;
153 static void askAboutAll(bool passwordFailure);
156 static void applicantChoice(CFUserNotificationRef userNotification, CFOptionFlags responseFlags)
158 ApplicantUIState choice;
160 if (kCFUserNotificationAlternateResponse == responseFlags) {
161 choice = ApplicantRejected;
162 } else if (kCFUserNotificationDefaultResponse == responseFlags) {
163 choice = ApplicantAccepted;
165 NSLog(@"Unexpected response %lu", responseFlags);
166 choice = ApplicantRejected;
170 CFErrorRef error = NULL;
171 NSArray *onScreen = applicantsInState(ApplicantOnScreen);
173 [onScreen enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
174 Applicant* applicant = (Applicant *) obj;
175 applicant.applicantUIState = choice;
178 if (choice == ApplicantRejected) {
179 // If this device has ever set up the public key this should work without the password...
180 processed = processRequests(&error);
182 NSLog(@"Didn't need password to process %@", onScreen);
183 cancelCurrentAlert(true);
186 // ...however if the public key gets lost we should "just" fall through to the validate
188 NSLog(@"Couldn't process reject without password (e=%@) for %@ (will try with password next)", error, onScreen);
190 CFReleaseNull(error);
193 NSString *password = (__bridge NSString *) CFUserNotificationGetResponseValue(userNotification, kCFUserNotificationTextFieldValuesKey, 0);
195 NSLog(@"No password given, retry");
199 const char *passwordUTF8 = [password UTF8String];
200 NSData *passwordBytes = [NSData dataWithBytes:passwordUTF8 length:strlen(passwordUTF8)];
202 // Sometimes securityd crashes between SOSCCRegisterUserCredentials and processRequests
203 // (which results in a process error -- I think this is 13355140), as a workaround we retry
204 // failure a few times before we give up.
205 for (int try = 0; try < 5 && !processed; try++) {
206 if (!SOSCCTryUserCredentials(CFSTR(""), (__bridge CFDataRef) passwordBytes, &error)) {
207 NSLog(@"Try user credentials failed %@", error);
208 if ((error == NULL) ||
209 (CFEqual(kSOSErrorDomain, CFErrorGetDomain(error)) && kSOSErrorWrongPassword == CFErrorGetCode(error))) {
210 NSLog(@"Calling askAboutAll again...");
211 [onScreen enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
212 Applicant *applicant = (Applicant*) obj;
213 applicant.applicantUIState = ApplicantWaiting;
216 CFReleaseNull(error);
219 EXIT_LOGGED_FAILURE(EX_DATAERR);
222 processed = processRequests(&error);
224 NSLog(@"Can't processRequests: %@ for %@", error, onScreen);
226 CFReleaseNull(error);
229 if (processed && firstApplicantWaitingOrOnScreen()) {
230 cancelCurrentAlert(false);
233 cancelCurrentAlert(true);
238 static void passwordFailurePrompt()
240 NSString *pwIncorrect = [NSString stringWithFormat:(NSString *)CFBridgingRelease(SecCopyCKString(SEC_CK_PASSWORD_INCORRECT)), appleIDAccountName()];
241 NSString *tryAgain = CFBridgingRelease(SecCopyCKString(SEC_CK_TRY_AGAIN));
242 NSDictionary *noteAttributes = @{
243 (id) kCFUserNotificationAlertHeaderKey : pwIncorrect,
244 (id) kCFUserNotificationDefaultButtonTitleKey : tryAgain,
245 (id) kCFUserNotificationAlertTopMostKey : @YES, // get us onto the lock screen
246 (__bridge id) SBUserNotificationDontDismissOnUnlock: @YES,
247 (__bridge id) SBUserNotificationDismissOnLock : @NO,
249 CFOptionFlags flags = kCFUserNotificationPlainAlertLevel;
251 CFUserNotificationRef note = CFUserNotificationCreate(NULL, 0.0, flags, &err, (__bridge CFDictionaryRef) noteAttributes);
254 CFUserNotificationReceiveResponse(note, 0.0, &flags);
260 static NSString *getLocalizedApprovalBody(NSString *deviceType) {
261 CFStringRef applicationReminder = NULL;
263 if ([deviceType isEqualToString:@"iPhone"])
264 applicationReminder = SecCopyCKString(SEC_CK_APPROVAL_BODY_IOS_IPHONE);
265 else if ([deviceType isEqualToString:@"iPod"])
266 applicationReminder = SecCopyCKString(SEC_CK_APPROVAL_BODY_IOS_IPOD);
267 else if ([deviceType isEqualToString:@"iPad"])
268 applicationReminder = SecCopyCKString(SEC_CK_APPROVAL_BODY_IOS_IPAD);
269 else if ([deviceType isEqualToString:@"Mac"])
270 applicationReminder = SecCopyCKString(SEC_CK_APPROVAL_BODY_IOS_MAC);
272 applicationReminder = SecCopyCKString(SEC_CK_APPROVAL_BODY_IOS_GENERIC);
274 return (__bridge_transfer NSString *) applicationReminder;
278 static NSDictionary *createNote(Applicant *applicantToAskAbout)
280 if(!applicantToAskAbout || !applicantToAskAbout.name || !applicantToAskAbout.deviceType)
283 NSString *header = [NSString stringWithFormat: (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_APPROVAL_TITLE_IOS), applicantToAskAbout.name];
284 NSString *body = [NSString stringWithFormat: getLocalizedApprovalBody(applicantToAskAbout.deviceType), appleIDAccountName()];
287 (id) kCFUserNotificationAlertHeaderKey : header,
288 (id) kCFUserNotificationAlertMessageKey : body,
289 (id) kCFUserNotificationDefaultButtonTitleKey : (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_ALLOW),
290 (id) kCFUserNotificationAlternateButtonTitleKey: (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_DONT_ALLOW),
291 (id) kCFUserNotificationTextFieldTitlesKey : (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_ICLOUD_PASSWORD),
292 (id) kCFUserNotificationAlertTopMostKey : @YES, // get us onto the lock screen
293 (__bridge_transfer id) SBUserNotificationDontDismissOnUnlock: @YES,
294 (__bridge_transfer id) SBUserNotificationDismissOnLock : @NO,
299 static void askAboutAll(bool passwordFailure)
301 if ([[MCProfileConnection sharedConnection] effectiveBoolValueForSetting: MCFeatureAccountModificationAllowed] == MCRestrictedBoolExplicitNo) {
302 NSLog(@"Account modifications not allowed.");
306 if (passwordFailure) {
307 passwordFailurePrompt();
310 if ((passwordFailure || !currentAlertIsForApplicants) && currentAlert) {
311 if (!currentAlertIsForApplicants) {
312 CFUserNotificationCancel(currentAlert);
314 // after password failure we need to remove the existing alert and supporting objects
315 // because we can't reuse them.
316 CFReleaseNull(currentAlert);
317 CFReleaseNull(currentAlertSource);
319 currentAlertIsForApplicants = true;
321 Applicant *applicantToAskAbout = firstApplicantWaitingOrOnScreen();
322 NSLog(@"Asking about: %@ (of: %@)", applicantToAskAbout, applicants);
324 NSDictionary *noteAttributes = createNote(applicantToAskAbout);
325 if(!noteAttributes) {
326 NSLog(@"NULL data for %@", applicantToAskAbout);
327 cancelCurrentAlert(true);
331 CFOptionFlags flags = flagsForAsk(applicantToAskAbout);
334 SInt32 err = CFUserNotificationUpdate(currentAlert, 0, flags, (__bridge CFDictionaryRef) noteAttributes);
336 NSLog(@"CFUserNotificationUpdate err=%d", (int)err);
337 EXIT_LOGGED_FAILURE(EX_SOFTWARE);
341 currentAlert = CFUserNotificationCreate(NULL, 0.0, flags, &err, (__bridge CFDictionaryRef) noteAttributes);
343 NSLog(@"Can't make notification for %@ err=%x", applicantToAskAbout, (int)err);
344 EXIT_LOGGED_FAILURE(EX_SOFTWARE);
347 currentAlertSource = CFUserNotificationCreateRunLoopSource(NULL, currentAlert, applicantChoice, 0);
348 CFRunLoopAddSource(CFRunLoopGetCurrent(), currentAlertSource, kCFRunLoopDefaultMode);
351 applicantToAskAbout.applicantUIState = ApplicantOnScreen;
355 static void scheduleActivity(int alertInterval)
357 xpc_object_t options = xpc_dictionary_create(NULL, NULL, 0);
358 xpc_dictionary_set_uint64(options, XPC_ACTIVITY_DELAY, alertInterval);
359 xpc_dictionary_set_uint64(options, XPC_ACTIVITY_GRACE_PERIOD, XPC_ACTIVITY_INTERVAL_1_MIN);
360 xpc_dictionary_set_bool(options, XPC_ACTIVITY_REPEATING, false);
361 xpc_dictionary_set_bool(options, XPC_ACTIVITY_ALLOW_BATTERY, true);
362 xpc_dictionary_set_string(options, XPC_ACTIVITY_PRIORITY, XPC_ACTIVITY_PRIORITY_UTILITY);
364 xpc_activity_register(kLaunchLaterXPCName, options, ^(xpc_activity_t activity) {
365 NSLog(@"activity handler fired");
370 static void reminderChoice(CFUserNotificationRef userNotification, CFOptionFlags responseFlags) {
371 if (responseFlags == kCFUserNotificationAlternateResponse || responseFlags == kCFUserNotificationDefaultResponse) {
372 PersistentState *state = [PersistentState loadFromStorage];
373 NSDate *nowish = [NSDate new];
374 state.pendingApplicationReminder = [nowish dateByAddingTimeInterval: state.pendingApplicationReminderAlertInterval];
375 scheduleActivity(state.pendingApplicationReminderAlertInterval);
376 [state writeToStorage];
377 if (responseFlags == kCFUserNotificationAlternateResponse) {
379 BOOL ok = [[LSApplicationWorkspace defaultWorkspace] openSensitiveURL:[NSURL URLWithString:castleKeychainUrl] withOptions:nil];
380 NSLog(@"%s iCSC: opening %@ ok=%d", __FUNCTION__, castleKeychainUrl, ok);
384 cancelCurrentAlert(true);
388 static bool iCloudResetAvailable() {
389 SecureBackup *backupd = [SecureBackup new];
390 NSDictionary *backupdResults;
391 NSError *error = [backupd getAccountInfoWithInfo:nil results:&backupdResults];
392 NSLog(@"SecureBackup e=%@ r=%@", error, backupdResults);
393 return (error == nil && [backupdResults[kSecureBackupIsEnabledKey] isEqualToNumber:@YES]);
398 static NSString *getLocalizedApplicationReminder() {
399 CFStringRef applicationReminder = NULL;
400 switch (MGGetSInt32Answer(kMGQDeviceClassNumber, MGDeviceClassInvalid)) {
401 case MGDeviceClassiPhone:
402 applicationReminder = SecCopyCKString(SEC_CK_REMINDER_BODY_IOS_IPHONE);
404 case MGDeviceClassiPod:
405 applicationReminder = SecCopyCKString(SEC_CK_REMINDER_BODY_IOS_IPOD);
407 case MGDeviceClassiPad:
408 applicationReminder = SecCopyCKString(SEC_CK_REMINDER_BODY_IOS_IPAD);
411 applicationReminder = SecCopyCKString(SEC_CK_REMINDER_BODY_IOS_GENERIC);
414 return (__bridge_transfer NSString *) applicationReminder;
418 static void postApplicationReminderAlert(NSDate *nowish, PersistentState *state, unsigned int alertInterval)
420 NSString *body = getLocalizedApplicationReminder();
421 bool has_iCSC = iCloudResetAvailable();
423 if (CPIsInternalDevice() &&
424 state.defaultPendingApplicationReminderAlertInterval != state.pendingApplicationReminderAlertInterval) {
426 body = [body stringByAppendingFormat: @"〖debug interval %u; wait time %@〗",
427 state.pendingApplicationReminderAlertInterval,
428 [nowish copyDescriptionOfIntervalSince:state.applicationDate]];
432 NSDictionary *pendingAttributes = @{
433 (id) kCFUserNotificationAlertHeaderKey : (__bridge NSString *) SecCopyCKString(SEC_CK_REMINDER_TITLE_IOS),
434 (id) kCFUserNotificationAlertMessageKey : body,
435 (id) kCFUserNotificationDefaultButtonTitleKey : (__bridge NSString *) SecCopyCKString(SEC_CK_REMINDER_BUTTON_OK),
436 (id) kCFUserNotificationAlternateButtonTitleKey: has_iCSC ? (__bridge NSString *) SecCopyCKString(SEC_CK_REMINDER_BUTTON_ICSC) : @"",
437 (id) kCFUserNotificationAlertTopMostKey : @YES,
438 (__bridge id) SBUserNotificationDontDismissOnUnlock : @YES,
439 (__bridge id) SBUserNotificationDismissOnLock : @NO,
440 (__bridge id) SBUserNotificationOneButtonPerLine : @YES,
443 currentAlert = CFUserNotificationCreate(NULL, 0.0, kCFUserNotificationPlainAlertLevel, &err, (__bridge CFDictionaryRef) pendingAttributes);
446 NSLog(@"Can't make pending notification err=%x", (int)err);
448 currentAlertIsForApplicants = false;
449 currentAlertSource = CFUserNotificationCreateRunLoopSource(NULL, currentAlert, reminderChoice, 0);
450 CFRunLoopAddSource(CFRunLoopGetCurrent(), currentAlertSource, kCFRunLoopDefaultMode);
455 static void kickOutChoice(CFUserNotificationRef userNotification, CFOptionFlags responseFlags) {
456 NSLog(@"kOC %@ %lu", userNotification, responseFlags);
457 if (responseFlags == kCFUserNotificationDefaultResponse) {
458 // We need to let things unwind to main for the new state to get saved
460 ACAccountStore *store = [ACAccountStore new];
461 ACAccount *primary = [store aa_primaryAppleAccount];
462 NSString *dsid = [primary aa_personID];
463 bool localICDP = false;
465 NSDictionary *options = @{ (__bridge id) kPCSSetupDSID : dsid, };
466 PCSIdentitySetRef identity = PCSIdentitySetCreate((__bridge CFDictionaryRef) options, NULL, NULL);
469 localICDP = PCSIdentitySetIsICDP(identity, NULL);
473 NSURL *url = [NSURL URLWithString: localICDP ? rejoinICDPUrl : castleKeychainUrl];
474 BOOL ok = [[LSApplicationWorkspace defaultWorkspace] openSensitiveURL:url withOptions:nil];
475 NSLog(@"ok=%d opening %@", ok, url);
478 cancelCurrentAlert(true);
482 CFStringRef const CJRAggdDepartureReasonKey = CFSTR("com.apple.security.circlejoinrequested.departurereason");
483 CFStringRef const CJRAggdNumCircleDevicesKey = CFSTR("com.apple.security.circlejoinrequested.numcircledevices");
485 static void postKickedOutAlert(enum DepartureReason reason)
487 NSString *header = nil;
488 NSString *message = nil;
490 // <rdar://problem/20358568> iCDP telemetry: ☂ Statistics on circle reset and drop out events
491 ADClientSetValueForScalarKey(CJRAggdDepartureReasonKey, reason);
493 int64_t num_peers = 0;
494 CFArrayRef peerList = SOSCCCopyPeerPeerInfo(NULL);
496 num_peers = CFArrayGetCount(peerList);
497 if (num_peers > 99) {
498 // Round down # peers to 2 significant digits
500 for (factor = 10; num_peers >= 100*factor; factor *= 10) ;
501 num_peers = (num_peers / factor) * factor;
505 ADClientSetValueForScalarKey(CJRAggdNumCircleDevicesKey, num_peers);
507 debugState = @"pKOA A";
508 syslog(LOG_ERR, "DepartureReason %d", reason);
510 case kSOSDiscoveredRetirement:
511 case kSOSLostPrivateKey:
512 case kSOSWithdrewMembership:
513 // Was: SEC_CK_CR_BODY_WITHDREW
514 // "... if you turn off a switch you have some idea why the light is off" - Murf
518 case kSOSNeverAppliedToCircle:
519 // We didn't get kicked out, we were never here. This should only happen if we changed iCloud accounts
520 // (and we had sync on in the previous one, and never had it on in the new one). As this is explicit
521 // user action alot of the "Light switch" argument (above) applies.
525 case kSOSNeverLeftCircle:
526 case kSOSMembershipRevoked:
527 case kSOSLeftUntrustedCircle:
529 header = (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_PWD_REQUIRED_TITLE);
530 message = (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_PWD_REQUIRED_BODY_IOS);
534 if (CPIsInternalDevice()) {
535 static const char *departureReasonStrings[] = {
536 "kSOSDepartureReasonError",
537 "kSOSNeverLeftCircle",
538 "kSOSWithdrewMembership",
539 "kSOSMembershipRevoked",
540 "kSOSLeftUntrustedCircle",
541 "kSOSNeverAppliedToCircle",
542 "kSOSDiscoveredRetirement",
543 "kSOSLostPrivateKey",
546 int idx = (kSOSDepartureReasonError <= reason && reason <= kSOSLostPrivateKey) ? reason : (kSOSLostPrivateKey + 1);
547 NSString *reason_str = [NSString stringWithFormat:(__bridge_transfer NSString *) SecCopyCKString(SEC_CK_CR_REASON_INTERNAL),
548 departureReasonStrings[idx]];
549 message = [message stringByAppendingString: reason_str];
552 NSDictionary *kickedAttributes = @{
553 (id) kCFUserNotificationAlertHeaderKey : header,
554 (id) kCFUserNotificationAlertMessageKey : message,
555 (id) kCFUserNotificationDefaultButtonTitleKey : (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_CONTINUE),
556 (id) kCFUserNotificationAlternateButtonTitleKey: (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_NOT_NOW),
557 (id) kCFUserNotificationAlertTopMostKey : @YES,
558 (__bridge id) SBUserNotificationDismissOnLock : @NO,
559 (__bridge id) SBUserNotificationDontDismissOnUnlock : @YES,
560 (__bridge id) SBUserNotificationOneButtonPerLine : @YES,
564 if (currentAlertIsForKickOut) {
565 debugState = @"pKOA B";
566 NSLog(@"Updating existing alert %@ with %@", currentAlert, kickedAttributes);
567 CFUserNotificationUpdate(currentAlert, 0, kCFUserNotificationPlainAlertLevel, (__bridge CFDictionaryRef) kickedAttributes);
569 debugState = @"pKOA C";
571 CFUserNotificationRef note = CFUserNotificationCreate(NULL, 0.0, kCFUserNotificationPlainAlertLevel, &err, (__bridge CFDictionaryRef) kickedAttributes);
572 assert((note == NULL) == (err != 0));
574 NSLog(@"Can't make kicked out notification err=%x", (int)err);
576 currentAlertIsForApplicants = false;
577 currentAlertIsForKickOut = true;
580 NSLog(@"New ko alert %@ a=%@", currentAlert, kickedAttributes);
581 currentAlertSource = CFUserNotificationCreateRunLoopSource(NULL, currentAlert, kickOutChoice, 0);
582 CFRunLoopAddSource(CFRunLoopGetCurrent(), currentAlertSource, kCFRunLoopDefaultMode);
583 int backupStateChangeToken;
584 notify_register_dispatch("com.apple.EscrowSecurityAlert.reset", &backupStateChangeToken, dispatch_get_main_queue(), ^(int token) {
585 if (currentAlert == note) {
586 NSLog(@"Backup state might have changed (dS=%@)", debugState);
587 postKickedOutAlert(reason);
589 NSLog(@"Backup state may have changed, but we don't care anymore (dS=%@)", debugState);
592 debugState = @"pKOA D";
594 debugState = @"pKOA E";
595 notify_cancel(backupStateChangeToken);
598 debugState = @"pKOA Z";
602 static bool processEvents()
604 debugState = @"processEvents A";
606 CFErrorRef error = NULL;
607 CFErrorRef departError = NULL;
608 SOSCCStatus circleStatus = SOSCCThisDeviceIsInCircle(&error);
609 NSDate *nowish = [NSDate date];
610 PersistentState *state = [PersistentState loadFromStorage];
611 enum DepartureReason departureReason = SOSCCGetLastDepartureReason(&departError);
612 NSLog(@"CircleStatus %d -> %d{%d} (s=%p)", state.lastCircleStatus, circleStatus, departureReason, state);
615 // Pending application reminder
616 NSTimeInterval timeUntilApplicationAlert = [state.pendingApplicationReminder timeIntervalSinceDate:nowish];
617 NSLog(@"Time until pendingApplicationReminder (%@) %f", [state.pendingApplicationReminder debugDescription], timeUntilApplicationAlert);
618 if (circleStatus == kSOSCCRequestPending) {
619 if (timeUntilApplicationAlert <= 0) {
620 debugState = @"reminderAlert";
621 postApplicationReminderAlert(nowish, state, state.pendingApplicationReminderAlertInterval);
623 scheduleActivity(ceil(timeUntilApplicationAlert));
628 // No longer in circle?
629 if ((state.lastCircleStatus == kSOSCCInCircle && (circleStatus == kSOSCCNotInCircle || circleStatus == kSOSCCCircleAbsent)) ||
630 (state.lastCircleStatus == kSOSCCCircleAbsent && circleStatus == kSOSCCNotInCircle && state.absentCircleWithNoReason) ||
631 state.debugShowLeftReason) {
632 // Used to be in the circle, now we aren't - tell the user why
633 debugState = @"processEvents B";
635 if (state.debugShowLeftReason) {
636 NSLog(@"debugShowLeftReason: %@", state.debugShowLeftReason);
637 departureReason = [state.debugShowLeftReason intValue];
638 state.debugShowLeftReason = nil;
639 CFReleaseNull(departError);
640 [state writeToStorage];
643 if (departureReason != kSOSDepartureReasonError) {
644 state.absentCircleWithNoReason = (circleStatus == kSOSCCCircleAbsent && departureReason == kSOSNeverLeftCircle);
645 NSLog(@"Depature reason %d", departureReason);
646 postKickedOutAlert(departureReason);
647 NSLog(@"pKOA returned (cS %d lCS %d)", circleStatus, state.lastCircleStatus);
649 NSLog(@"Couldn't get last departure reason: %@", departError);
654 // Circle applications: pending request(s) started / completed
655 debugState = @"processEvents C";
656 if (circleStatus != state.lastCircleStatus) {
657 SOSCCStatus lastCircleStatus = state.lastCircleStatus;
658 state.lastCircleStatus = circleStatus;
660 if (lastCircleStatus != kSOSCCRequestPending && circleStatus == kSOSCCRequestPending) {
661 NSLog(@"Pending request started");
662 state.applicationDate = nowish;
663 state.pendingApplicationReminder = [nowish dateByAddingTimeInterval: state.pendingApplicationReminderAlertInterval];
664 scheduleActivity(state.pendingApplicationReminderAlertInterval);
666 if (lastCircleStatus == kSOSCCRequestPending && circleStatus != kSOSCCRequestPending) {
667 NSLog(@"Pending request completed");
668 state.applicationDate = [NSDate distantPast];
669 state.pendingApplicationReminder = [NSDate distantFuture];
672 [state writeToStorage];
675 if (circleStatus != kSOSCCInCircle) {
676 if (circleStatus == kSOSCCRequestPending && currentAlert) {
678 CFUserNotificationRef postedAlert = currentAlert;
680 debugState = @"processEvents D1";
681 notify_register_dispatch(kSOSCCCircleChangedNotification, ¬ifyToken, dispatch_get_main_queue(), ^(int token) {
682 if (postedAlert != currentAlert) {
683 NSLog(@"-- CC after original alert gone (currentAlertIsForApplicants %d, pA %p, cA %p -- %@)",
684 currentAlertIsForApplicants, postedAlert, currentAlert, currentAlert);
685 notify_cancel(token);
687 CFErrorRef localError = NULL;
688 SOSCCStatus newCircleStatus = SOSCCThisDeviceIsInCircle(&localError);
689 if (newCircleStatus != kSOSCCRequestPending) {
690 if (newCircleStatus == kSOSCCError)
691 NSLog(@"No longer pending (nCS=%d, alert=%@) error: %@", newCircleStatus, currentAlert, localError);
693 NSLog(@"No longer pending (nCS=%d, alert=%@)", newCircleStatus, currentAlert);
694 cancelCurrentAlert(true);
696 NSLog(@"Still pending...");
698 CFReleaseNull(localError);
701 debugState = @"processEvents D2";
702 NSLog(@"NOTE: currentAlertIsForApplicants %d, token %d", currentAlertIsForApplicants, notifyToken);
706 debugState = @"processEvents D4";
707 NSLog(@"SOSCCThisDeviceIsInCircle status %d, not checking applicants", circleStatus);
713 debugState = @"processEvents E";
714 applicants = [NSMutableDictionary new];
715 for (id applicantInfo in (__bridge_transfer NSArray *) SOSCCCopyApplicantPeerInfo(&error)) {
716 Applicant *applicant = [[Applicant alloc] initWithPeerInfo:(__bridge SOSPeerInfoRef) applicantInfo];
717 applicants[applicant.idString] = applicant;
720 // Log error from SOSCCCopyApplicantPeerInfo() above?
721 CFReleaseNull(error);
723 int notify_token = -42;
724 debugState = @"processEvents F";
725 int notify_register_status = notify_register_dispatch(kSOSCCCircleChangedNotification, ¬ify_token, dispatch_get_main_queue(), ^(int token) {
726 NSLog(@"Notified: %s", kSOSCCCircleChangedNotification);
727 CFErrorRef circleStatusError = NULL;
729 bool needsUpdate = false;
730 CFErrorRef copyPeerError = NULL;
731 NSMutableSet *newIds = [NSMutableSet new];
732 for (id applicantInfo in (__bridge_transfer NSArray *) SOSCCCopyApplicantPeerInfo(©PeerError)) {
733 Applicant *newApplicant = [[Applicant alloc] initWithPeerInfo:(__bridge SOSPeerInfoRef) applicantInfo];
734 [newIds addObject:newApplicant.idString];
735 Applicant *existingApplicant = applicants[newApplicant.idString];
736 if (existingApplicant) {
737 switch (existingApplicant.applicantUIState) {
738 case ApplicantWaiting:
739 applicants[newApplicant.idString] = newApplicant;
742 case ApplicantOnScreen:
743 newApplicant.applicantUIState = ApplicantOnScreen;
744 applicants[newApplicant.idString] = newApplicant;
748 NSLog(@"Update to %@ >> %@ with pending order, should work out ok though", existingApplicant, newApplicant);
753 applicants[newApplicant.idString] = newApplicant;
757 NSLog(@"Could not update peer info array: %@", copyPeerError);
758 CFRelease(copyPeerError);
762 NSMutableArray *idsToRemoveFromApplicants = [NSMutableArray new];
763 for (NSString *exisitngId in [applicants keyEnumerator]) {
764 if (![newIds containsObject:exisitngId]) {
765 [idsToRemoveFromApplicants addObject:exisitngId];
769 [applicants removeObjectsForKeys:idsToRemoveFromApplicants];
771 if (newIds.count == 0) {
772 NSLog(@"All applicants were handled elsewhere");
773 cancelCurrentAlert(true);
775 SOSCCStatus currentCircleStatus = SOSCCThisDeviceIsInCircle(&circleStatusError);
776 if (kSOSCCInCircle != currentCircleStatus) {
777 NSLog(@"Left circle (%d), not handling remaining %lu applicants", currentCircleStatus, (unsigned long)newIds.count);
778 cancelCurrentAlert(true);
783 NSLog(@"needsUpdate false, not updating alert");
785 // Log circleStatusError?
786 CFReleaseNull(circleStatusError);
788 NSLog(@"ACC token %d, status %d", notify_token, notify_register_status);
789 debugState = @"processEvents F2";
791 if (applicants.count == 0) {
792 NSLog(@"No applicants");
794 debugState = @"processEvents F3";
796 debugState = @"processEvents F4";
798 debugState = @"processEvents F5";
803 debugState = @"processEvents F6";
804 notify_cancel(notify_token);
805 debugState = @"processEvents DONE";
811 int main (int argc, const char * argv[]) {
812 xpc_transaction_begin();
815 // NOTE: DISPATCH_QUEUE_PRIORITY_LOW will not actually manage to drain events in a lot of cases (like circleStatus != kSOSCCInCircle)
816 xpc_set_event_stream_handler("com.apple.notifyd.matching", dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^(xpc_object_t object) {
817 char *event_description = xpc_copy_description(object);
818 NSLog(@"notifyd event: %s\nAlert (%p) %s %s\ndebugState: %@", event_description, currentAlert,
819 currentAlertIsForApplicants ? "for applicants" : "!applicants",
820 currentAlertIsForKickOut ? "KO" : "!KO", debugState);
821 free(event_description);
824 xpc_activity_register(kLaunchLaterXPCName, XPC_ACTIVITY_CHECK_IN, ^(xpc_activity_t activity) {
828 while (falseInARow < 2) {
829 if (processEvents()) {
834 cancelCurrentAlert(false);
835 if (doOnceInMainBlockChain) {
836 doOnceInMainBlockChain();
837 doOnceInMainBlockChain = NULL;
843 xpc_transaction_end();