]> git.saurik.com Git - apple/security.git/blobdiff - CircleJoinRequested/CircleJoinRequested.m
Security-57031.1.35.tar.gz
[apple/security.git] / CircleJoinRequested / CircleJoinRequested.m
diff --git a/CircleJoinRequested/CircleJoinRequested.m b/CircleJoinRequested/CircleJoinRequested.m
new file mode 100644 (file)
index 0000000..e00c7bc
--- /dev/null
@@ -0,0 +1,771 @@
+//
+//  CircleJoinRequested.m
+//  CircleJoinRequested
+//
+//  Created by J Osborne on 3/5/13.
+//  Copyright (c) 2013-2014 Apple Inc. All Rights Reserved.
+//
+#import <Accounts/Accounts.h>
+#import <Accounts/ACAccountStore_Private.h>
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wnewline-eof"
+#import <AppleAccount/AppleAccount.h>
+#pragma clang diagnostic pop
+#import <AppleAccount/ACAccountStore+AppleAccount.h>
+#import <Accounts/ACAccountType_Private.h>
+#import <Foundation/Foundation.h>
+#include <dispatch/dispatch.h>
+#include "SecureObjectSync/SOSCloudCircle.h"
+#include "SecureObjectSync/SOSPeerInfo.h"
+#import <CoreFoundation/CFUserNotification.h>
+#import <SpringBoardServices/SBSCFUserNotificationKeys.h>
+#include <notify.h>
+#include <sysexits.h>
+#import "Applicant.h"
+#import "NSArray+map.h"
+#import <ManagedConfiguration/MCProfileConnection.h>
+#import <ManagedConfiguration/MCFeatures.h>
+#import <Security/SecFrameworkStrings.h>
+#import "PersistantState.h"
+#include <xpc/private.h>
+#include <sys/time.h>
+#import "NSDate+TimeIntervalDescription.h"
+#include <MobileGestalt.h>
+#include <xpc/activity.h>
+#include <xpc/private.h>
+#import <MobileCoreServices/MobileCoreServices.h>
+#import <MobileCoreServices/LSApplicationWorkspace.h>
+#import <CloudServices/SecureBackup.h>
+#import <syslog.h>
+
+// As long as we are logging the failure use exit code of zero to make launchd happy
+#define EXIT_LOGGED_FAILURE(code)  xpc_transaction_end();  exit(0)
+
+const char *kLaunchLaterXPCName = "com.apple.security.CircleJoinRequestedTick";
+CFRunLoopSourceRef currentAlertSource = NULL;
+CFUserNotificationRef currentAlert = NULL;
+bool currentAlertIsForApplicants = true;
+bool currentAlertIsForKickOut = false;
+NSMutableDictionary *applicants = nil;
+volatile NSString *debugState = @"main?";
+dispatch_block_t doOnceInMainBlockChain = NULL;
+
+NSString *castleKeychainUrl = @"prefs:root=CASTLE&path=Keychain/ADVANCED";
+
+#if 0
+// For use with:      __attribute__((cleanup(CFReleaseSafeIndirect))) CFType auto_var;
+static void CFReleaseSafeIndirect(void *o_)
+{
+    void **o = o_;
+    if (o && *o) {
+        CFRelease(*o);
+    }
+}
+#endif
+
+static void doOnceInMain(dispatch_block_t block)
+{
+    if (doOnceInMainBlockChain) {
+        doOnceInMainBlockChain = ^{
+            doOnceInMainBlockChain();
+            block();
+        };
+    } else {
+        doOnceInMainBlockChain = block;
+    }
+}
+
+static NSString *appleIDAccountName()
+{
+    ACAccountStore *accountStore = [[ACAccountStore alloc] init];
+    ACAccount *primaryAppleAccount = [accountStore aa_primaryAppleAccount];
+    return primaryAppleAccount.username;
+}
+
+static CFOptionFlags flagsForAsk(Applicant *applicant)
+{
+       return kCFUserNotificationPlainAlertLevel|CFUserNotificationSecureTextField(0);
+}
+
+// NOTE: gives precedence to OnScreen
+static Applicant *firstApplicantWaitingOrOnScreen()
+{
+    Applicant *waiting = nil;
+    for (Applicant *applicant in [applicants objectEnumerator]) {
+        if (applicant.applicantUIState == ApplicantOnScreen) {
+            return applicant;
+        } else if (applicant.applicantUIState == ApplicantWaiting) {
+            waiting = applicant;
+        }
+    }
+    
+    return waiting;
+}
+
+static NSMutableArray *applicantsInState(ApplicantUIState state)
+{
+    NSMutableArray *results = [NSMutableArray new];
+    for (Applicant *applicant in [applicants objectEnumerator]) {
+        if (applicant.applicantUIState == state) {
+            [results addObject:applicant];
+        }
+    }
+    
+    return results;
+}
+
+static BOOL processRequests(CFErrorRef *error)
+{
+    bool ok = true;
+    NSMutableArray *toAccept = [[applicantsInState(ApplicantAccepted) mapWithBlock:^id(id obj) {
+        return (id)[obj rawPeerInfo];
+    }] mutableCopy];
+    NSMutableArray *toReject = [[applicantsInState(ApplicantRejected) mapWithBlock:^id(id obj) {
+        return (id)[obj rawPeerInfo];
+    }] mutableCopy];
+    
+    NSLog(@"Process accept: %@", toAccept);
+    NSLog(@"Process reject: %@", toReject);
+    
+    if ([toAccept count]) {
+        ok = ok && SOSCCAcceptApplicants((__bridge CFArrayRef)(toAccept), error);
+    }
+    if ([toReject count]) {
+        ok = ok && SOSCCRejectApplicants((__bridge CFArrayRef)(toReject), error);
+    }
+    
+    return ok;
+}
+
+static void cancelCurrentAlert(bool stopRunLoop)
+{
+    if (currentAlertSource) {
+        CFRunLoopRemoveSource(CFRunLoopGetCurrent(), currentAlertSource, kCFRunLoopDefaultMode);
+        CFRelease(currentAlertSource);
+        currentAlertSource = NULL;
+    }
+    if (currentAlert) {
+        CFUserNotificationCancel(currentAlert);
+        CFRelease(currentAlert);
+        currentAlert = NULL;
+    }
+    if (stopRunLoop) {
+        CFRunLoopStop(CFRunLoopGetCurrent());
+    }
+    currentAlertIsForKickOut = currentAlertIsForApplicants = false;
+}
+
+static void askAboutAll(bool passwordFailure);
+
+static void applicantChoice(CFUserNotificationRef userNotification, CFOptionFlags responseFlags)
+{
+    ApplicantUIState choice;
+    
+    if (kCFUserNotificationAlternateResponse == responseFlags) {
+        choice = ApplicantRejected;
+    } else if (kCFUserNotificationDefaultResponse == responseFlags) {
+        choice = ApplicantAccepted;
+    } else {
+        NSLog(@"Unexpected response %lu", responseFlags);
+        choice = ApplicantRejected;
+    }
+    
+    BOOL processed = NO;
+    CFErrorRef error = NULL;
+    
+    NSArray *onScreen = applicantsInState(ApplicantOnScreen);
+    
+    [onScreen enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
+        Applicant* applicant = (Applicant*) obj;
+        
+        applicant.applicantUIState = choice;
+    }];
+    
+    if (choice == ApplicantRejected) {
+        // If this device has ever set up the public key this should work without the password...
+        processed = processRequests(&error);
+        if (processed) {
+            NSLog(@"Didn't need password to process %@", onScreen);
+            cancelCurrentAlert(true);
+            return;
+        } else {
+            // ...however if the public key gets lost we should "just" fall through to the validate
+            // password path.
+            NSLog(@"Couldn't process reject without password (e=%@) for %@ (will try with password next)", error, onScreen);
+        }
+    }
+    
+    NSString *password = (__bridge NSString *)(CFUserNotificationGetResponseValue(userNotification, kCFUserNotificationTextFieldValuesKey, 0));
+    if (!password) {
+        NSLog(@"No password given, retry");
+        askAboutAll(true);
+        return;
+    }
+    const char *passwordUTF8 = [password UTF8String];
+    NSData *passwordBytes = [NSData dataWithBytes:passwordUTF8 length:strlen(passwordUTF8)];
+    
+    // Sometimes securityd crashes between the SOSCCRegisterUserCredentials and the processRequests,
+    // (which results in a process error -- I think this is 13355140); as a workaround we retry
+    // failure a few times before we give up.
+    for (int try = 0; try < 5 && !processed; try++) {
+        if (!SOSCCTryUserCredentials(CFSTR(""), (__bridge CFDataRef)(passwordBytes), &error)) {
+            NSLog(@"Try user credentials failed %@", error);
+            if ((error==NULL) || (CFEqual(kSOSErrorDomain, CFErrorGetDomain(error)) && kSOSErrorWrongPassword == CFErrorGetCode(error))) {
+                NSLog(@"Calling askAboutAll again...");
+                
+                [onScreen enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
+                    Applicant* applicant = (Applicant*) obj;
+                    
+                    applicant.applicantUIState = ApplicantWaiting;
+                }];
+                askAboutAll(true);
+                return;
+            }
+            EXIT_LOGGED_FAILURE(EX_DATAERR);
+        }
+        
+        processed = processRequests(&error);
+        if (!processed) {
+            NSLog(@"Can't processRequests: %@ for %@", error, onScreen);
+        }
+    }
+    if (processed && firstApplicantWaitingOrOnScreen()) {
+        cancelCurrentAlert(false);
+        askAboutAll(false);
+    } else {
+        cancelCurrentAlert(true);
+    }
+}
+
+static void passwordFailurePrompt()
+{
+    //CFBridgingRelease
+    NSString *pwIncorrect = [NSString stringWithFormat:(NSString *)CFBridgingRelease(SecCopyCKString(SEC_CK_PASSWORD_INCORRECT)), appleIDAccountName()];
+    NSString *tryAgain = CFBridgingRelease(SecCopyCKString(SEC_CK_TRY_AGAIN));
+    NSDictionary *noteAttributes = @{
+                                     (id)kCFUserNotificationAlertHeaderKey: pwIncorrect,
+                                     (id)kCFUserNotificationDefaultButtonTitleKey: tryAgain,
+                                     // TopMost gets us onto the lock screen
+                                     (id)kCFUserNotificationAlertTopMostKey: (id)kCFBooleanTrue,
+                                     (__bridge id)SBUserNotificationDontDismissOnUnlock: @YES,
+                                     (__bridge id)SBUserNotificationDismissOnLock: @NO,
+                                     };
+    CFOptionFlags flags = kCFUserNotificationPlainAlertLevel;
+    SInt32 err;
+    CFUserNotificationRef note = CFUserNotificationCreate(NULL, 0.0, flags, &err, (__bridge CFDictionaryRef)noteAttributes);
+    CFUserNotificationReceiveResponse(note, 0.0, &flags);
+    CFRelease(note);
+}
+
+static NSDictionary *createNote(Applicant *applicantToAskAbout) {
+    if(!applicantToAskAbout) return NULL;
+    NSString *appName = applicantToAskAbout.name;
+    if(!appName) return NULL;
+    NSString *devType = applicantToAskAbout.deviceType;
+    if(!devType) return NULL;
+    return @{
+        (id)kCFUserNotificationAlertHeaderKey: [NSString stringWithFormat:(__bridge_transfer NSString*)SecCopyCKString(SEC_CK_JOIN_TITLE), appName],
+        (id)kCFUserNotificationAlertMessageKey: [NSString stringWithFormat:(__bridge_transfer NSString*)SecCopyCKString(SEC_CK_JOIN_PROMPT), appleIDAccountName(), devType],
+        (id)kCFUserNotificationDefaultButtonTitleKey: (__bridge_transfer NSString*)SecCopyCKString(SEC_CK_ALLOW),
+        (id)kCFUserNotificationAlternateButtonTitleKey: (__bridge_transfer NSString*)SecCopyCKString(SEC_CK_DONT_ALLOW),
+        (id)kCFUserNotificationTextFieldTitlesKey: (__bridge_transfer NSString*)SecCopyCKString(SEC_CK_ICLOUD_PASSWORD),
+        // TopMost gets us onto the lock screen
+        (id)kCFUserNotificationAlertTopMostKey: (id)kCFBooleanTrue,
+        (__bridge_transfer id)SBUserNotificationDontDismissOnUnlock: @YES,
+        (__bridge_transfer id)SBUserNotificationDismissOnLock: @NO,
+    };
+}
+
+static void askAboutAll(bool passwordFailure)
+{
+    if ([[MCProfileConnection sharedConnection] effectiveBoolValueForSetting: MCFeatureAccountModificationAllowed] == MCRestrictedBoolExplicitNo) {
+        NSLog(@"Account modifications not allowed.");
+        return;
+    }
+    
+    if (passwordFailure) {
+        passwordFailurePrompt();
+    }
+    
+    if ((passwordFailure || !currentAlertIsForApplicants) && currentAlert) {
+        if (!currentAlertIsForApplicants) {
+            CFUserNotificationCancel(currentAlert);
+        }
+        // after password failure we need to remove the existing alert and supporting objects
+        // because we can't reuse them.
+        CFRelease(currentAlert);
+        currentAlert = NULL;
+        if (currentAlertSource) {
+            CFRelease(currentAlertSource);
+            currentAlertSource = NULL;
+        }
+    }
+    currentAlertIsForApplicants = true;
+    
+    Applicant *applicantToAskAbout = firstApplicantWaitingOrOnScreen();
+    NSLog(@"Asking about: %@ (of: %@)", applicantToAskAbout, applicants);
+    
+    NSDictionary *noteAttributes = createNote(applicantToAskAbout);
+    if(!noteAttributes) {
+        NSLog(@"NULL data for %@", applicantToAskAbout);
+        cancelCurrentAlert(true);
+        return;
+    }
+    
+    CFOptionFlags flags = flagsForAsk(applicantToAskAbout);
+    
+    if (currentAlert) {
+        SInt32 err = CFUserNotificationUpdate(currentAlert, 0, flags, (__bridge CFDictionaryRef)noteAttributes);
+        if (err) {
+            NSLog(@"CFUserNotificationUpdate err=%d", (int)err);
+            EXIT_LOGGED_FAILURE(EX_SOFTWARE);
+        }
+    } else {
+        SInt32 err = 0;
+        currentAlert = CFUserNotificationCreate(NULL, 0.0, flags, &err, (__bridge CFDictionaryRef)(noteAttributes));
+        if (err) {
+            NSLog(@"Can't make notification for %@ err=%x", applicantToAskAbout, (int)err);
+            EXIT_LOGGED_FAILURE(EX_SOFTWARE);
+        }
+        
+        currentAlertSource = CFUserNotificationCreateRunLoopSource(NULL, currentAlert, applicantChoice, 0);
+        CFRunLoopAddSource(CFRunLoopGetCurrent(), currentAlertSource, kCFRunLoopDefaultMode);
+    }
+    
+    applicantToAskAbout.applicantUIState = ApplicantOnScreen;
+}
+
+static void scheduleActivity(int alertInterval)
+{
+    xpc_object_t options = xpc_dictionary_create(NULL, NULL, 0);
+    xpc_dictionary_set_uint64(options, XPC_ACTIVITY_DELAY, alertInterval);
+    xpc_dictionary_set_uint64(options, XPC_ACTIVITY_GRACE_PERIOD, XPC_ACTIVITY_INTERVAL_1_MIN);
+    xpc_dictionary_set_bool(options, XPC_ACTIVITY_REPEATING, false);
+    xpc_dictionary_set_bool(options, XPC_ACTIVITY_ALLOW_BATTERY, true);
+    xpc_dictionary_set_string(options, XPC_ACTIVITY_PRIORITY, XPC_ACTIVITY_PRIORITY_UTILITY);
+    
+    xpc_activity_register(kLaunchLaterXPCName, options, ^(xpc_activity_t activity) {
+        NSLog(@"activity handler fired");
+    });
+}
+
+static void reminderChoice(CFUserNotificationRef userNotification, CFOptionFlags responseFlags)
+{
+    if (kCFUserNotificationAlternateResponse == responseFlags || kCFUserNotificationDefaultResponse == responseFlags) {
+        PersistantState *state = [PersistantState loadFromStorage];
+        NSDate *nowish = [NSDate new];
+        state.pendingApplicationReminder = [nowish dateByAddingTimeInterval: state.pendingApplicationReminderAlertInterval];
+        scheduleActivity(state.pendingApplicationReminderAlertInterval);
+        [state writeToStorage];
+        if (kCFUserNotificationAlternateResponse == responseFlags) {
+            BOOL ok = [[LSApplicationWorkspace defaultWorkspace] openSensitiveURL:[NSURL URLWithString:castleKeychainUrl] withOptions:nil];
+            NSLog(@"ok=%d opening %@", ok, [NSURL URLWithString:castleKeychainUrl]);
+        }
+    }
+
+    cancelCurrentAlert(true);
+}
+
+
+static NSString* getLocalizedDeviceClass(void) {
+    NSString *deviceType = NULL;
+    switch (MGGetSInt32Answer(kMGQDeviceClassNumber, MGDeviceClassInvalid)) {
+        case MGDeviceClassiPhone:
+            deviceType = (__bridge NSString*)SecCopyCKString(SEC_CK_THIS_IPHONE);
+            break;
+        case MGDeviceClassiPod:
+            deviceType = (__bridge NSString*)SecCopyCKString(SEC_CK_THIS_IPOD);
+            break;
+        case MGDeviceClassiPad:
+            deviceType = (__bridge NSString*)SecCopyCKString(SEC_CK_THIS_IPAD);
+            break;
+        default:
+            deviceType = (__bridge NSString*)SecCopyCKString(SEC_CK_THIS_DEVICE);
+            break;
+    }
+    return deviceType;
+}
+
+static bool iCloudResetAvailable()
+{
+    SecureBackup *backupd = [SecureBackup new];
+    NSDictionary *backupdResults;
+    NSError *error = [backupd getAccountInfoWithInfo:nil results:&backupdResults];
+    NSLog(@"SecureBackup e=%@ r=%@", error, backupdResults);
+    return (nil == error && [backupdResults[kSecureBackupIsEnabledKey] isEqualToNumber:@YES]);
+}
+
+static void postApplicationReminderAlert(NSDate *nowish, PersistantState *state, unsigned int alertInterval)
+{
+
+    NSString *deviceType = getLocalizedDeviceClass();
+    
+    bool has_iCSC = iCloudResetAvailable();
+
+    NSString *alertMessage = [NSString stringWithFormat:(__bridge NSString*)SecCopyCKString(has_iCSC ? SEC_CK_ARS1_BODY : SEC_CK_ARS0_BODY), deviceType];
+    if (state.defaultPendingApplicationReminderAlertInterval != state.pendingApplicationReminderAlertInterval) {
+        alertMessage = [NSString stringWithFormat:@"%@  〖debug interval %u; wait time %@〗",
+                        alertMessage,
+                        state.pendingApplicationReminderAlertInterval,
+                        [nowish copyDescriptionOfIntervalSince:state.applcationDate]];
+    }
+    
+    NSDictionary *pendingAttributes = @{
+                                        (id)kCFUserNotificationAlertHeaderKey: (__bridge NSString*)SecCopyCKString(has_iCSC ? SEC_CK_ARS1_TITLE : SEC_CK_ARS0_TITLE),
+                                        (id)kCFUserNotificationAlertMessageKey: alertMessage,
+                                        (id)kCFUserNotificationDefaultButtonTitleKey: (__bridge NSString*)SecCopyCKString(SEC_CK_AR_APPROVE_OTHER),
+                                        (id)kCFUserNotificationAlternateButtonTitleKey: has_iCSC ? (__bridge NSString*)SecCopyCKString(SEC_CK_AR_USE_CODE) : @"",
+                                        (id)kCFUserNotificationAlertTopMostKey: (id)kCFBooleanTrue,
+                                        (__bridge id)SBUserNotificationHideButtonsInAwayView: @YES,
+                                        (__bridge id)SBUserNotificationDontDismissOnUnlock: @YES,
+                                        (__bridge id)SBUserNotificationDismissOnLock: @NO,
+                                                                               (__bridge id)SBUserNotificationOneButtonPerLine: @YES,
+                                        };
+    SInt32 err = 0;
+    currentAlert = CFUserNotificationCreate(NULL, 0.0, kCFUserNotificationPlainAlertLevel, &err, (__bridge CFDictionaryRef)(pendingAttributes));
+    
+    if (err) {
+        NSLog(@"Can't make pending notification err=%x", (int)err);
+    } else {
+        currentAlertIsForApplicants = false;
+        currentAlertSource = CFUserNotificationCreateRunLoopSource(NULL, currentAlert, reminderChoice, 0);
+        CFRunLoopAddSource(CFRunLoopGetCurrent(), currentAlertSource, kCFRunLoopDefaultMode);
+    }
+}
+
+static void kickOutChoice(CFUserNotificationRef userNotification, CFOptionFlags responseFlags)
+{
+    NSLog(@"kOC %@ %lu", userNotification, responseFlags);
+    if (kCFUserNotificationAlternateResponse == responseFlags) {
+        // We need to let things unwind to main for the new state to get saved
+        doOnceInMain(^{
+            BOOL ok = [[LSApplicationWorkspace defaultWorkspace] openSensitiveURL:[NSURL URLWithString:castleKeychainUrl] withOptions:nil];
+            NSLog(@"ok=%d opening %@", ok, [NSURL URLWithString:castleKeychainUrl]);
+        });
+    }
+    cancelCurrentAlert(true);
+}
+
+static void postKickedOutAlert(enum DepartureReason reason)
+{
+    NSString *deviceType = getLocalizedDeviceClass();
+       NSString *message = nil;
+    debugState = @"pKOA A";
+       bool ok_to_use_code = iCloudResetAvailable();
+    debugState = @"pKOA B";
+   
+       switch (reason) {
+               case kSOSNeverLeftCircle:
+            // Was: SEC_CK_CR_BODY_NEVER_LEFT
+            return;
+                       break;
+                       
+               case kSOSWithdrewMembership:
+            // Was: SEC_CK_CR_BODY_WITHDREW
+            // "... if you turn off a switch you have some idea why the light is off" - Murf
+            return;
+                       break;
+                       
+               case kSOSMembershipRevoked:
+                       message = [NSString stringWithFormat:(__bridge NSString*)SecCopyCKString(SEC_CK_CR_BODY_REVOKED), deviceType];
+                       break;
+                       
+               case kSOSLeftUntrustedCircle:
+                       message = [NSString stringWithFormat:(__bridge NSString*)SecCopyCKString(SEC_CK_CR_BODY_LEFT_UNTRUSTED), deviceType];
+                       ok_to_use_code = false;
+                       break;
+                       
+        case kSOSNeverAppliedToCircle:
+            // We didn't get kicked out, we were never here.   This should only happen if we changed iCloud accounts
+            // (and we had sync on in the previous one, and never had it on in the new one).   As this is explicit
+            // user action alot of thd "Light switch" argument (above) applies.
+            return;
+            break;
+            
+               default:
+                       message = [NSString stringWithFormat:(__bridge NSString*)SecCopyCKString(SEC_CK_CR_BODY_UNKNOWN), deviceType];
+                       ok_to_use_code = false;
+            syslog(LOG_ERR, "Unknown DepartureReason %d", reason);
+                       break;
+       }
+       
+    NSDictionary *kickedAttributes = @{
+                                       (id)kCFUserNotificationAlertHeaderKey: (__bridge NSString*)SecCopyCKString(SEC_CK_CR_TITLE),
+                                       (id)kCFUserNotificationAlertMessageKey: message,
+                                       (id)kCFUserNotificationDefaultButtonTitleKey: (__bridge NSString*)SecCopyCKString(SEC_CK_CR_OK),
+                                       (id)kCFUserNotificationAlternateButtonTitleKey: ok_to_use_code ? (__bridge NSString*)SecCopyCKString(SEC_CK_CR_USE_CODE)
+                                       : @"",
+                                       (id)kCFUserNotificationAlertTopMostKey: (id)kCFBooleanTrue,
+                                       (__bridge id)SBUserNotificationHideButtonsInAwayView: @YES,
+                                       (__bridge id)SBUserNotificationDontDismissOnUnlock: @YES,
+                                       (__bridge id)SBUserNotificationDismissOnLock: @NO,
+                                       (__bridge id)SBUserNotificationOneButtonPerLine: @YES,
+                                       };
+    SInt32 err = 0;
+    
+    if (currentAlertIsForKickOut) {
+        debugState = @"pKOA C";
+        NSLog(@"Updating existing alert %@ with %@", currentAlert, kickedAttributes);
+        CFUserNotificationUpdate(currentAlert, 0, kCFUserNotificationPlainAlertLevel, (__bridge CFDictionaryRef)(kickedAttributes));
+    } else {
+        debugState = @"pKOA D";
+
+        CFUserNotificationRef note = CFUserNotificationCreate(NULL, 0.0, kCFUserNotificationPlainAlertLevel, &err, (__bridge CFDictionaryRef)(kickedAttributes));
+        if (err) {
+            NSLog(@"Can't make kicked out notification err=%x", (int)err);
+        } else {
+            currentAlertIsForApplicants = false;
+            currentAlertIsForKickOut = true;
+            
+            currentAlert = note;
+            NSLog(@"New ko alert %@ a=%@", currentAlert, kickedAttributes);
+            currentAlertSource = CFUserNotificationCreateRunLoopSource(NULL, currentAlert, kickOutChoice, 0);
+            CFRunLoopAddSource(CFRunLoopGetCurrent(), currentAlertSource, kCFRunLoopDefaultMode);
+            int backupStateChangeToken;
+            notify_register_dispatch("com.apple.EscrowSecurityAlert.reset", &backupStateChangeToken, dispatch_get_main_queue(), ^(int token) {
+                if (currentAlert == note) {
+                    NSLog(@"Backup state might have changed (dS=%@)", debugState);
+                    postKickedOutAlert(reason);
+                } else {
+                    NSLog(@"Backup state may have changed, but we don't care anymore (dS=%@)", debugState);
+                }
+            });
+            debugState = @"pKOA E";
+            CFRunLoopRun();
+            debugState = @"pKOA F";
+            notify_cancel(backupStateChangeToken);
+        }
+    }
+    debugState = @"pKOA Z";
+}
+
+static bool processEvents()
+{
+    debugState = @"processEvents A";
+    CFErrorRef error = NULL;
+    CFErrorRef departError = NULL;
+    SOSCCStatus circleStatus = SOSCCThisDeviceIsInCircle(&error);
+    NSDate *nowish = [NSDate date];
+    PersistantState *state = [PersistantState loadFromStorage];
+    enum DepartureReason departureReason = SOSCCGetLastDepartureReason(&departError);
+    NSLog(@"CircleStatus %d -> %d{%d} (s=%p)", state.lastCircleStatus, circleStatus, departureReason, state);
+
+    NSTimeInterval timeUntilApplicationAlert = [state.pendingApplicationReminder timeIntervalSinceDate:nowish];
+    
+    NSLog(@"Time until pendingApplicationReminder (%@) %f", [state.pendingApplicationReminder debugDescription], timeUntilApplicationAlert);
+    
+    if (circleStatus == kSOSCCRequestPending && timeUntilApplicationAlert <= 0) {
+        debugState = @"reminderAlert";
+        postApplicationReminderAlert(nowish, state, state.pendingApplicationReminderAlertInterval);
+    } else if (circleStatus == kSOSCCRequestPending) {
+        scheduleActivity(ceil(timeUntilApplicationAlert));
+    }
+    
+    if (((circleStatus == kSOSCCNotInCircle || circleStatus == kSOSCCCircleAbsent) && state.lastCircleStatus == kSOSCCInCircle) || state.debugShowLeftReason || (circleStatus == kSOSCCNotInCircle && state.lastCircleStatus == kSOSCCCircleAbsent && state.absentCircleWithNoReason)) {
+        debugState = @"processEvents B";
+        // Use to be in the circle, now we aren't.   We ought to tell the user why.
+        
+        if (state.debugShowLeftReason) {
+            NSLog(@"debugShowLeftReason is %@", state.debugShowLeftReason);
+            departureReason = [state.debugShowLeftReason intValue];
+            state.debugShowLeftReason = nil;
+            departError = NULL;
+            [state writeToStorage];
+        }
+        
+        if (kSOSDepartureReasonError != departureReason) {
+            if (circleStatus == kSOSCCCircleAbsent && departureReason == kSOSNeverLeftCircle) {
+                // We don't yet know why the circle has vanished, remember our current ignorance
+                state.absentCircleWithNoReason = YES;
+            } else {
+                state.absentCircleWithNoReason = NO;
+            }
+            NSLog(@"Depature reason %d", departureReason);
+            postKickedOutAlert(departureReason);
+            NSLog(@"pKOA returned (cS %d lCS %d)", circleStatus, state.lastCircleStatus);
+        } else {
+            NSLog(@"Can't get last depature reason: %@", departError);
+        }
+    }
+    
+    debugState = @"processEvents C";
+    
+    if (circleStatus != state.lastCircleStatus) {
+        SOSCCStatus lastCircleStatus = state.lastCircleStatus;
+        state.lastCircleStatus = circleStatus;
+        
+        if (lastCircleStatus != kSOSCCRequestPending && circleStatus == kSOSCCRequestPending) {
+            state.applcationDate = nowish;
+            state.pendingApplicationReminder = [nowish dateByAddingTimeInterval: state.pendingApplicationReminderAlertInterval];
+            scheduleActivity(state.pendingApplicationReminderAlertInterval);
+        }
+        if (lastCircleStatus == kSOSCCRequestPending && circleStatus != kSOSCCRequestPending) {
+            NSLog(@"Pending request completed");
+            state.applcationDate = [NSDate distantPast];
+            state.pendingApplicationReminder = [NSDate distantFuture];
+        }
+        
+        [state writeToStorage];
+    }
+    
+    if (circleStatus != kSOSCCInCircle) {
+        if (circleStatus == kSOSCCRequestPending && currentAlert) {
+            int notifyToken = 0;
+            CFUserNotificationRef postedAlert = currentAlert;
+            
+            debugState = @"processEvents D1";
+            notify_register_dispatch(kSOSCCCircleChangedNotification, &notifyToken, dispatch_get_main_queue(), ^(int token) {
+                if (postedAlert != currentAlert) {
+                    NSLog(@"-- CC after original alert gone (currentAlertIsForApplicants %d, pA %p, cA %p -- %@)", currentAlertIsForApplicants ? 1:0, postedAlert, currentAlert, currentAlert);
+                    notify_cancel(token);
+                } else {
+                    CFErrorRef localError = NULL;
+                    SOSCCStatus newCircleStatus = SOSCCThisDeviceIsInCircle(&localError);
+                    if (newCircleStatus != kSOSCCRequestPending) {
+                        if (newCircleStatus == kSOSCCError)
+                            NSLog(@"No longer pending (nCS=%d, alert=%@) error: %@", newCircleStatus, currentAlert, localError);
+                        else
+                            NSLog(@"No longer pending (nCS=%d, alert=%@)", newCircleStatus, currentAlert);
+                        cancelCurrentAlert(true);
+                    } else {
+                        NSLog(@"Still pending...");
+                    }
+                }
+            });
+            debugState = @"processEvents D2";
+            NSLog(@"NOTE: currentAlertIsForApplicants %d, token %d", currentAlertIsForApplicants ? 1:0, notifyToken);
+            CFRunLoopRun();
+            return true;
+        }
+        debugState = @"processEvents D3";
+        NSLog(@"SOSCCThisDeviceIsInCircle status %d, not checking applicants", circleStatus);
+        return false;
+    }
+    
+    debugState = @"processEvents E";
+    applicants = [NSMutableDictionary new];
+    for (id applicantInfo in (__bridge_transfer NSArray *)(SOSCCCopyApplicantPeerInfo(&error))) {
+        Applicant *applicant = [[Applicant alloc] initWithPeerInfo:(__bridge SOSPeerInfoRef)(applicantInfo)];
+        applicants[applicant.idString] = applicant;
+    }
+    
+    int notify_token = -42;
+    debugState = @"processEvents F";
+    int notify_register_status = notify_register_dispatch(kSOSCCCircleChangedNotification, &notify_token, dispatch_get_main_queue(), ^(int token) {
+        NSLog(@"Notified: %s", kSOSCCCircleChangedNotification);
+        CFErrorRef circleStatusError = NULL;
+        
+        bool needsUpdate = false;
+        CFErrorRef copyPeerError = NULL;
+        NSMutableSet *newIds = [NSMutableSet new];
+        for (id applicantInfo in (__bridge_transfer NSArray *)(SOSCCCopyApplicantPeerInfo(&copyPeerError))) {
+            Applicant *newApplicant = [[Applicant alloc] initWithPeerInfo:(__bridge SOSPeerInfoRef)(applicantInfo)];
+            [newIds addObject:newApplicant.idString];
+            Applicant *existingApplicant = applicants[newApplicant.idString];
+            if (existingApplicant) {
+                switch (existingApplicant.applicantUIState) {
+                    case ApplicantWaiting:
+                        applicants[newApplicant.idString] = newApplicant;
+                        break;
+                        
+                    case ApplicantOnScreen:
+                        newApplicant.applicantUIState = ApplicantOnScreen;
+                        applicants[newApplicant.idString] = newApplicant;
+                        break;
+                        
+                    default:
+                        NSLog(@"Update to %@ >> %@ with pending order, should work out Ok though", existingApplicant, newApplicant);
+                        break;
+                }
+            } else {
+                needsUpdate = true;
+                applicants[newApplicant.idString] = newApplicant;
+            }
+        }
+        if (copyPeerError) {
+            NSLog(@"Could not update peer info array: %@", copyPeerError);
+            return;
+        }
+        
+        NSMutableArray *idsToRemoveFromApplicants = [NSMutableArray new];
+        for (NSString *exisitngId in [applicants keyEnumerator]) {
+            if (![newIds containsObject:exisitngId]) {
+                [idsToRemoveFromApplicants addObject:exisitngId];
+                needsUpdate = true;
+            }
+        }
+        [applicants removeObjectsForKeys:idsToRemoveFromApplicants];
+        
+        if (newIds.count == 0) {
+            NSLog(@"All applicants were handled elsewhere");
+            cancelCurrentAlert(true);
+        }
+        SOSCCStatus currentCircleStatus = SOSCCThisDeviceIsInCircle(&circleStatusError);
+        if (kSOSCCInCircle != currentCircleStatus) {
+            NSLog(@"Left circle (%d), not handing remaining %lu applicants", currentCircleStatus, (unsigned long)newIds.count);
+            cancelCurrentAlert(true);
+        }
+        if (needsUpdate) {
+            askAboutAll(false);
+        } else {
+            NSLog(@"needsUpdate false, not updating alert");
+        }
+    });
+    NSLog(@"ACC token %d, status %d", notify_token, notify_register_status);
+    debugState = @"processEvents F2";
+    
+    if (applicants.count == 0) {
+        NSLog(@"No applicants");
+    } else {
+        debugState = @"processEvents F3";
+        askAboutAll(false);
+        debugState = @"processEvents F4";
+        if (currentAlert) {
+            debugState = @"processEvents F5";
+            CFRunLoopRun();
+        }
+    }
+    
+    debugState = @"processEvents F6";
+    notify_cancel(notify_token);
+    debugState = @"processEvents DONE";
+    
+    return false;
+}
+
+int main (int argc, const char * argv[])
+{
+    @autoreleasepool {
+        xpc_transaction_begin();
+        
+        // NOTE: DISPATCH_QUEUE_PRIORITY_LOW will not actually manage to drain events
+        // in a lot of cases (like circleStatus != kSOSCCInCircle)
+        xpc_set_event_stream_handler("com.apple.notifyd.matching", dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^(xpc_object_t object) {
+            char *event_description = xpc_copy_description(object);
+            NSLog(@"notifyd event: %s\nAlert (%p) %s %s\ndebugState: %@", event_description, currentAlert, currentAlertIsForApplicants ? "for applicants" : "!applicants", currentAlertIsForKickOut ? "KO" : "!KO", debugState);
+            free(event_description);
+        });
+        
+        xpc_activity_register(kLaunchLaterXPCName, XPC_ACTIVITY_CHECK_IN, ^(xpc_activity_t activity) {
+        });
+        
+        
+        int falseInARow = 0;
+        while (falseInARow < 2) {
+            if (processEvents()) {
+                falseInARow = 0;
+            } else {
+                falseInARow++;
+            }
+            cancelCurrentAlert(false);
+            if (doOnceInMainBlockChain) {
+                doOnceInMainBlockChain();
+                doOnceInMainBlockChain = NULL;
+            }
+        }
+       }
+    
+    NSLog(@"Done");
+    xpc_transaction_end();
+    return(0);
+}