KNAppDelegate.m   [plain text]

/*
* Copyright (c) 2013-2014 Apple Inc. All Rights Reserved.
*
* @APPLE_LICENSE_HEADER_START@
*
* This file contains Original Code and/or Modifications of Original Code
* as defined in and that are subject to the Apple Public Source License
* Version 2.0 (the 'License'). You may not use this file except in
* compliance with the License. Please obtain a copy of the License at
* http://www.opensource.apple.com/apsl/ and read it before using this
* file.
*
* The Original Code and all software distributed under the License are
* distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
* EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
* INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
* Please see the License for the specific language governing rights and
* limitations under the License.
*
* @APPLE_LICENSE_HEADER_END@
*/

#import "KNAppDelegate.h"
#import "KDSecCircle.h"
#import "KDCirclePeer.h"
#import "NSDictionary+compactDescription.h"
#import <AOSUI/NSImageAdditions.h>
#import <AppleSystemInfo/AppleSystemInfo.h>
#import <Security/SecFrameworkStrings.h>
#import "notify.h"
#import <utilities/debugging.h>
#import <os/variant_private.h>

#import <Accounts/Accounts.h>
#import <AOSAccounts/MobileMePrefsCoreAEPrivate.h>
#import <AOSAccounts/MobileMePrefsCore.h>
#import <AOSAccounts/ACAccountStore+iCloudAccount.h>
#import <AOSAccounts/iCloudAccount.h>

#include <msgtracer_client.h>
#include <msgtracer_keys.h>
#include <CrashReporterSupport/CrashReporterSupportPrivate.h>
#import <ProtectedCloudStorage/CloudIdentity.h>
#import "CoreCDP/CDPFollowUpController.h"
#import "CoreCDP/CDPFollowUpContext.h"
#import <CoreCDP/CDPAccount.h>

static const char * const kLaunchLaterXPCName = "com.apple.security.Keychain-Circle-Notification-TICK";
static const NSString * const kKickedOutKey = @"KickedOut";
static const NSString * const kValidOnlyOutOfCircleKey = @"ValidOnlyOutOfCircle";
static const NSString * const kPasswordChangedOrTrustedDeviceChanged = @"TDorPasswordChanged";
static NSString *prefpane = @"/System/Library/PreferencePanes/iCloudPref.prefPane";
#define kPublicKeyNotAvailable "com.apple.security.publickeynotavailable"
#define kPublicKeyAvailable "com.apple.security.publickeyavailable"
static NSString *KeychainPCDetailsAEAction = @"AKPCDetailsAEAction";
bool _hasPostedAndStillInError = false;
bool _haveCheckedForICDPStatusOnceInCircle = false;
bool _isAccountICDP = false;

@implementation KNAppDelegate

static NSUserNotificationCenter *appropriateNotificationCenter()
{
return [NSUserNotificationCenter _centerForIdentifier: @"com.apple.security.keychain-circle-notification"
type: _NSUserNotificationCenterTypeSystem];
}

static void PSKeychainSyncIsUsingICDP(void)
{
ACAccountStore *accountStore = [[ACAccountStore alloc] init];
ACAccount *primaryiCloudAccount = nil;

if ([accountStore respondsToSelector:@selector(icaPrimaryAppleAccount)]){
primaryiCloudAccount = [accountStore icaPrimaryAppleAccount];
}

NSString *dsid = primaryiCloudAccount.icaPersonID;
BOOL isICDPEnabled = NO;
if (dsid) {
isICDPEnabled = [CDPAccount isICDPEnabledForDSID:dsid];
NSLog(@"iCDP: PSKeychainSyncIsUsingICDP returning %@", isICDPEnabled ? @"TRUE" : @"FALSE");
} else {
NSLog(@"iCDP: no primary account");
}

_isAccountICDP = isICDPEnabled;
}

-(void) startFollowupKitRepair
{
NSError *localError = NULL;
CDPFollowUpController *cdpd = [[CDPFollowUpController alloc] init];
CDPFollowUpContext *context = [CDPFollowUpContext contextForStateRepair];
[cdpd postFollowUpWithContext:context error:&localError ];
if(localError){
secnotice("kcn", "request to CoreCDP to follow up failed: %@", localError);
}
else{
secnotice("kcn", "CoreCDP handling follow up");
_hasPostedAndStillInError = false;
}
}

- (void) handleDismissedNotification
{
if(_isAccountICDP){
secnotice("kcn", "handling dismissed notification, would start a follow up");
[self startFollowupKitRepair];
}
else
secerror("unable to find primary account");
}

- (void) notifyiCloudPreferencesAbout: (NSString *) eventName
{
if (eventName == nil)
return;

secnotice("kcn", "notifyiCloudPreferencesAbout %@", eventName);

NSString *accountID = (__bridge_transfer NSString*)(MMCopyLoggedInAccountFromAccounts());
ACAccountStore *accountStore = [[ACAccountStore alloc] init];
ACAccount *primaryiCloudAccount = nil;

if ([accountStore respondsToSelector:@selector(icaPrimaryAppleAccount)]){
primaryiCloudAccount = [accountStore icaPrimaryAppleAccount];
}

if(primaryiCloudAccount){
AEDesc aeDesc;
BOOL createdAEDesc = createAEDescWithAEActionAndAccountID((__bridge NSString *) kMMServiceIDKeychainSync, eventName, accountID, &aeDesc);
if (createdAEDesc) {
NSArray *prefPaneURL = [NSArray arrayWithObject: [NSURL fileURLWithPath: prefpane ]];

LSLaunchURLSpec lsSpec = {
.appURL = NULL,
.itemURLs = (__bridge CFArrayRef)prefPaneURL,
.passThruParams = &aeDesc,
.launchFlags = kLSLaunchDefaults | kLSLaunchAsync,
.asyncRefCon = NULL,
};

OSErr err = LSOpenFromURLSpec(&lsSpec, NULL);

if (err)
secerror("Can't send event %@, err=%d", eventName, err);
AEDisposeDesc(&aeDesc);
} else {
secerror("unable to create and send aedesc for account: '%@' and action: '%@'\n", primaryiCloudAccount, eventName);
}
}
secerror("unable to find primary account");
}

- (void) timerCheck
{
NSDate *nowish = [NSDate new];

self.state = [KNPersistentState loadFromStorage];
if ([nowish compare:self.state.pendingApplicationReminder] != NSOrderedAscending) {
secnotice("kcn", "REMINDER TIME: %@ >>> %@", nowish, self.state.pendingApplicationReminder);

// self.circle.rawStatus might not be valid yet
if (SOSCCThisDeviceIsInCircle(NULL) == kSOSCCRequestPending) {
// Still have a request pending, send reminder, and also in addtion to the UI
// we need to send a notification for iCloud pref pane to pick up
CFNotificationCenterPostNotificationWithOptions(
CFNotificationCenterGetDistributedCenter(),
CFSTR("com.apple.security.secureobjectsync.pendingApplicationReminder"),
(__bridge const void *) [self.state.applicationDate description], NULL, 0
);

[self postApplicationReminder];
self.state.pendingApplicationReminder = [nowish dateByAddingTimeInterval:[self getPendingApplicationReminderInterval]];
[self.state writeToStorage];
}
}
}

- (void) scheduleActivityAt: (NSDate *) time
{
if ([time compare:[NSDate distantFuture]] != NSOrderedSame) {
NSTimeInterval howSoon = [time timeIntervalSinceNow];
if (howSoon > 0)
[self scheduleActivityIn:ceil(howSoon)];
else
[self timerCheck];
}
}

- (void) scheduleActivityIn: (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) {
[self timerCheck];
});
}

- (NSTimeInterval) getPendingApplicationReminderInterval
{
if (self.state.pendingApplicationReminderInterval)
return [self.state.pendingApplicationReminderInterval doubleValue];
else
return 24*60*60;
}

#define ICKC_EVENT_DISABLED "com.apple.security.secureobjectsync.disabled"
#define ICKC_EVENT_DEPARTURE_REASON "com.apple.security.secureobjectsync.departurereason"
#define ICKC_EVENT_NUM_PEERS "com.apple.security.secureobjectsync.numcircledevices"

- (void) applicationDidFinishLaunching: (NSNotification *) aNotification
{
appropriateNotificationCenter().delegate = self;
int out_taken;
int available;
secnotice("kcn", "Posted at launch: %@", appropriateNotificationCenter().deliveredNotifications);

notify_register_dispatch(kPublicKeyAvailable, &available, dispatch_get_main_queue(), ^(int token) {
CFErrorRef err = NULL;
KNAppDelegate *me = self;
SOSCCStatus currentCircleStatus = SOSCCThisDeviceIsInCircle(&err);
me.state = [KNPersistentState loadFromStorage];

secnotice("kcn", "got public key available notification");

me.state.lastCircleStatus = currentCircleStatus;

[me.state writeToStorage];
});

//register for public key not available notification, if occurs KCN can react
notify_register_dispatch(kPublicKeyNotAvailable, &out_taken, dispatch_get_main_queue(), ^(int token) {
CFErrorRef err = NULL;
KNAppDelegate *me = self;
enum DepartureReason departureReason = SOSCCGetLastDepartureReason(&err);
SOSCCStatus currentCircleStatus = SOSCCThisDeviceIsInCircle(&err);
me.state = [KNPersistentState loadFromStorage];

secnotice("kcn", "got public key not available notification, but won't send notification unless circle transition matches");
secnotice("kcn", "current circle status: %d, current departure reason: %d, last circle status: %d", currentCircleStatus, departureReason, me.state.lastCircleStatus);

PSKeychainSyncIsUsingICDP();

if(_isAccountICDP){
if((currentCircleStatus == kSOSCCError || currentCircleStatus == kSOSCCCircleAbsent || currentCircleStatus == kSOSCCNotInCircle) && _hasPostedAndStillInError == false) {
secnotice("kcn", "iCDP: device not in circle, posting follow up");
[self postRequirePassword];
_hasPostedAndStillInError = true;
}
else if(currentCircleStatus == kSOSCCInCircle){
secnotice("kcn", "iCDP: device is in circle!");
_hasPostedAndStillInError = false;
}
}
else if(!_isAccountICDP && currentCircleStatus == kSOSCCError && me.state.lastCircleStatus == kSOSCCInCircle && (departureReason == kSOSNeverLeftCircle)) {
secnotice("kcn", "circle status went from in circle to not in circle");
[self postRequirePassword];
}
me.state.lastCircleStatus = currentCircleStatus;

[me.state writeToStorage];
});

self.viewedIds = [NSMutableSet new];
self.circle = [KDSecCircle new];
KNAppDelegate *me = self;

[self.circle addChangeCallback:^{
secnotice("kcn", "{ChangeCallback}");

CFErrorRef err = NULL;

NSDate *nowish = [NSDate date];
enum DepartureReason departureReason = SOSCCGetLastDepartureReason(&err);
SOSCCStatus circleStatus = SOSCCThisDeviceIsInCircle(&err);
me.state = [KNPersistentState loadFromStorage];
secnotice("kcn", "applicationDidFinishLaunching");

PSKeychainSyncIsUsingICDP();

if(_isAccountICDP){
if((circleStatus == kSOSCCError || circleStatus == kSOSCCCircleAbsent || circleStatus == kSOSCCNotInCircle) && _hasPostedAndStillInError == false) {

secnotice("kcn", "ICDP: We need the password to re-validate ourselves - it's changed on another device");
me.state.lastCircleStatus = circleStatus;
[me.state writeToStorage];
[me postRequirePassword];
_hasPostedAndStillInError = true;
}
else if(circleStatus == kSOSCCInCircle){
secnotice("kcn", "iCDP: device is in circle!");
_hasPostedAndStillInError = false;
}
}
else if(!_isAccountICDP && circleStatus == kSOSCCError && me.state.lastCircleStatus == kSOSCCInCircle && (departureReason == kSOSNeverLeftCircle)) {
secnotice("kcn", "SA: circle status went from in circle to not in circle");
[me postRequirePassword];
}
// Pending application reminder
secnotice("kcn", "{ChangeCallback} scheduleActivity %@", me.state.pendingApplicationReminder);
if (circleStatus == kSOSCCRequestPending)
[me scheduleActivityAt:me.state.pendingApplicationReminder];

// No longer in circle?
if ((me.state.lastCircleStatus == kSOSCCInCircle && (circleStatus == kSOSCCNotInCircle || circleStatus == kSOSCCCircleAbsent)) ||
(me.state.lastCircleStatus == kSOSCCCircleAbsent && circleStatus == kSOSCCNotInCircle && me.state.absentCircleWithNoReason) ||
me.state.debugLeftReason) {
enum DepartureReason reason = kSOSNeverLeftCircle;
if (me.state.debugLeftReason) {
reason = [me.state.debugLeftReason intValue];
me.state.debugLeftReason = nil;
[me.state writeToStorage];
} else {
reason = SOSCCGetLastDepartureReason(&err);
if (reason == kSOSDepartureReasonError) {
secnotice("kcn", "SOSCCGetLastDepartureReason err: %@", err);
}
if (err) CFRelease(err);
}

if (reason != kSOSDepartureReasonError) {
// Post kick-out alert

// <rdar://problem/20862435> MessageTracer data to find out how many users were dropped & reset
msgtracer_domain_t domain = msgtracer_domain_new(ICKC_EVENT_DISABLED);
msgtracer_msg_t mt_msg = NULL;

if (domain != NULL)
mt_msg = msgtracer_msg_new(domain);

if (mt_msg) {
char s[16];

msgtracer_set(mt_msg, kMsgTracerKeySignature, ICKC_EVENT_DEPARTURE_REASON);
snprintf(s, sizeof(s), "%u", reason);
msgtracer_set(mt_msg, kMsgTracerKeyValue, s);

int64_t num_peers = 0;
CFArrayRef peerList = SOSCCCopyPeerPeerInfo(NULL);
if (peerList) {
num_peers = CFArrayGetCount(peerList);
if (num_peers > 99) {
// Round down # peers to 2 significant digits
int factor;
for (factor = 10; num_peers >= 100*factor; factor *= 10) ;
num_peers = (num_peers / factor) * factor;
}
CFRelease(peerList);
}
msgtracer_set(mt_msg, kMsgTracerKeySignature2, ICKC_EVENT_NUM_PEERS);
snprintf(s, sizeof(s), "%lld", num_peers);
msgtracer_set(mt_msg, kMsgTracerKeyValue2, s);

msgtracer_set(mt_msg, kMsgTracerKeySummarize, "NO");
msgtracer_log(mt_msg, ASL_LEVEL_DEBUG, "");
}

// FIXME:
// 1. Write here due to [me timerCheck] => [KNPersistentState loadFromStorage] below?!?
// 2. Or change call order of timerCheck, pendingApplication reminder below???
me.state.absentCircleWithNoReason = (circleStatus == kSOSCCCircleAbsent && reason == kSOSNeverLeftCircle);
[me.state writeToStorage];
secnotice("kcn", "{ChangeCallback} departure reason %d", reason);

switch (reason) {
case kSOSDiscoveredRetirement:
case kSOSLostPrivateKey:
case kSOSWithdrewMembership:
case kSOSNeverAppliedToCircle:
break;

case kSOSNeverLeftCircle:
case kSOSMembershipRevoked:
case kSOSLeftUntrustedCircle:
default:
[me postKickedOutAlert: reason];
break;
}
}
}

// Circle applications: pending request(s) started / completed
if (me.circle.rawStatus != me.state.lastCircleStatus) {
SOSCCStatus lastCircleStatus = me.state.lastCircleStatus;
me.state.lastCircleStatus = circleStatus;

if (lastCircleStatus != kSOSCCRequestPending && circleStatus == kSOSCCRequestPending) {
secnotice("kcn", "{ChangeCallback} Pending request START");
me.state.applicationDate = nowish;
me.state.pendingApplicationReminder = [me.state.applicationDate dateByAddingTimeInterval:[me getPendingApplicationReminderInterval]];
[me.state writeToStorage]; // FIXME: move below? might be needed for scheduleActivityAt...
[me scheduleActivityAt:me.state.pendingApplicationReminder];
}

if (lastCircleStatus == kSOSCCRequestPending && circleStatus != kSOSCCRequestPending) {
secnotice("kcn", "Pending request completed");
me.state.applicationDate = [NSDate distantPast];
me.state.pendingApplicationReminder = [NSDate distantFuture];
[me.state writeToStorage];

// Remove reminders
NSUserNotificationCenter *noteCenter = appropriateNotificationCenter();
for (NSUserNotification *note in noteCenter.deliveredNotifications) {
if (note.userInfo[(NSString*) kValidOnlyOutOfCircleKey] && note.userInfo[@"ApplicationReminder"]) {
secnotice("kcn", "{ChangeCallback} Removing notification %@", note);
[appropriateNotificationCenter() removeDeliveredNotification: note];
}
}
}
}

// Clear out (old) reset notifications
if (me.circle.isInCircle) {
secnotice("kcn", "{ChangeCallback} me.circle.isInCircle");
NSUserNotificationCenter *noteCenter = appropriateNotificationCenter();
for (NSUserNotification *note in noteCenter.deliveredNotifications) {
if (note.userInfo[(NSString*) kValidOnlyOutOfCircleKey]) {
secnotice("kcn", "Removing existing notification (%@) now that we are in circle", note);
[appropriateNotificationCenter() removeDeliveredNotification: note];
}
}
}

//Clear out (old) password changed notifications
if(me.circle.isInCircle){
secnotice("kcn", "{ChangeCallback} me.circle.isInCircle");
NSUserNotificationCenter *noteCenter = appropriateNotificationCenter();
for (NSUserNotification *note in noteCenter.deliveredNotifications) {
if (note.userInfo[(NSString*) kPasswordChangedOrTrustedDeviceChanged]) {
secnotice("kcn", "Removing existing notification (%@) now that we are valid again", note);
[appropriateNotificationCenter() removeDeliveredNotification: note];
}
}

}

// Applicants
secnotice("kcn", "{ChangeCallback} Applicants");
NSMutableSet *applicantIds = [NSMutableSet new];
for (KDCirclePeer *applicant in me.circle.applicants) {
if (!me.circle.isInCircle) {
// Don't yammer on about circles we aren't in, and don't announce our own
// join requests as if the user could approve them locally!
break;
}
[me postForApplicant:applicant];
[applicantIds addObject:applicant.idString];
}

// Update notifications
NSUserNotificationCenter *notificationCenter = appropriateNotificationCenter();
secnotice("kcn", "Checking validity of %lu notes", (unsigned long)notificationCenter.deliveredNotifications.count);
for (NSUserNotification *note in notificationCenter.deliveredNotifications) {
if (note.userInfo[@"applicantId"] && ![applicantIds containsObject:note.userInfo[@"applicantId"]]) {
secnotice("kcn", "No longer an applicant (%@) for %@ (I=%@)", note.userInfo[@"applicantId"], note, [note.userInfo compactDescription]);
[notificationCenter removeDeliveredNotification:note];
} else {
secnotice("kcn", "Still an applicant (%@) for %@ (I=%@)", note.userInfo[@"applicantId"], note, [note.userInfo compactDescription]);
}
}

me.state.lastCircleStatus = circleStatus;

[me.state writeToStorage];
}];
}

- (BOOL) userNotificationCenter: (NSUserNotificationCenter *) center
shouldPresentNotification: (NSUserNotification *) notification
{
return YES;
}

- (void) userNotificationCenter: (NSUserNotificationCenter *) center
didActivateNotification: (NSUserNotification *) notification
{
if (notification.activationType == NSUserNotificationActivationTypeActionButtonClicked) {
[self notifyiCloudPreferencesAbout:notification.userInfo[@"Activate"]];
}
}

- (void) userNotificationCenter: (NSUserNotificationCenter *) center
didDismissAlert: (NSUserNotification *) notification
{
[self handleDismissedNotification];

// If we don't do anything here & another notification comes in we
// will repost the alert, which will be dumb.
id applicantId = notification.userInfo[@"applicantId"];
if (applicantId != nil) {
[self.viewedIds addObject:applicantId];
}
}

- (void) postForApplicant: (KDCirclePeer *) applicant
{
static int postCount = 0;

if ([self.viewedIds containsObject:applicant.idString]) {
secnotice("kcn", "Already viewed %@, skipping", applicant);
return;
}

NSUserNotificationCenter *noteCenter = appropriateNotificationCenter();
for (NSUserNotification *note in noteCenter.deliveredNotifications) {
if ([applicant.idString isEqualToString:note.userInfo[@"applicantId"]]) {
if (note.isPresented) {
secnotice("kcn", "Already posted&presented: %@ (I=%@)", note, note.userInfo);
return;
} else {
secnotice("kcn", "Already posted, but not presented: %@ (I=%@)", note, note.userInfo);
}
}
}

NSUserNotification *note = [NSUserNotification new];
note.title = [NSString stringWithFormat: (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_APPROVAL_TITLE), applicant.name];
note.informativeText = [KNAppDelegate localisedApprovalBodyWithDeviceTypeFromPeerInfo:applicant.peerObject];
note._displayStyle = _NSUserNotificationDisplayStyleAlert;
note._identityImage = [NSImage bundleImage];
note._identityImageStyle = _NSUserNotificationIdentityImageStyleRectangleNoBorder;
note.otherButtonTitle = (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_DECLINE);
note.actionButtonTitle = (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_APPROVE);
note.identifier = [[NSUUID new] UUIDString];
note.userInfo = @{
@"applicantName": applicant.name,
@"applicantId" : applicant.idString,
@"Activate" : (__bridge NSString *) kMMPropertyKeychainAADetailsAEAction,
};

secnotice("kcn", "About to post #%d/%lu (%@): %@", postCount, noteCenter.deliveredNotifications.count, applicant.idString, note);
[appropriateNotificationCenter() deliverNotification:note];
[self.viewedIds addObject:applicant.idString];
postCount++;
}

+ (NSString *)localisedApprovalBodyWithDeviceTypeFromPeerInfo:(id)peerInfo {
NSString *type = (__bridge NSString *)SOSPeerInfoGetPeerDeviceType((__bridge SOSPeerInfoRef)(peerInfo));
CFStringRef localisedType = NULL;
if ([type isEqualToString:@"iPad"]) {
localisedType = SecCopyCKString(SEC_CK_APPROVAL_BODY_OSX_IPAD);
} else if ([type isEqualToString:@"iPhone"]) {
localisedType = SecCopyCKString(SEC_CK_APPROVAL_BODY_OSX_IPHONE);
} else if ([type isEqualToString:@"iPod"]) {
localisedType = SecCopyCKString(SEC_CK_APPROVAL_BODY_OSX_IPOD);
} else if ([type isEqualToString:@"Mac"]) {
localisedType = SecCopyCKString(SEC_CK_APPROVAL_BODY_OSX_MAC);
} else {
localisedType = SecCopyCKString(SEC_CK_APPROVAL_BODY_OSX_GENERIC);
}
return (__bridge_transfer NSString *)localisedType;
}

- (void) postRequirePassword
{
if(!_isAccountICDP){
NSUserNotificationCenter *noteCenter = appropriateNotificationCenter();
for (NSUserNotification *note in noteCenter.deliveredNotifications) {
if (note.userInfo[(NSString*) kPasswordChangedOrTrustedDeviceChanged]) {
if (note.isPresented) {
secnotice("kcn", "Already posted & presented: %@", note);
[appropriateNotificationCenter() removeDeliveredNotification: note];
} else {
secnotice("kcn", "Already posted, but not presented: %@", note);
}
}
}

NSString *message = CFBridgingRelease(SecCopyCKString(SEC_CK_PWD_REQUIRED_BODY_OSX));
if (os_variant_has_internal_ui("iCloudKeychain")) {
NSString *reason_str = [NSString stringWithFormat:(__bridge_transfer NSString *) SecCopyCKString(SEC_CK_CR_REASON_INTERNAL), @"Device became untrusted or password changed"];
message = [message stringByAppendingString: reason_str];
}

NSUserNotification *note = [NSUserNotification new];
note.title = (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_PWD_REQUIRED_TITLE);
note.informativeText = message;
note._identityImage = [NSImage bundleImage];
note._identityImageStyle = _NSUserNotificationIdentityImageStyleRectangleNoBorder;
note.otherButtonTitle = (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_NOT_NOW);
note.actionButtonTitle = (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_CONTINUE);
note.identifier = [[NSUUID new] UUIDString];

note.userInfo = @{
kPasswordChangedOrTrustedDeviceChanged : @1,
@"Activate" : (__bridge NSString *) kMMPropertyKeychainPCDetailsAEAction,
};

secnotice("kcn", "body=%@", note.informativeText);
secnotice("kcn", "About to post #-/%lu (PASSWORD/TRUSTED DEVICE): %@", noteCenter.deliveredNotifications.count, note);
[appropriateNotificationCenter() deliverNotification:note];
}
else{
secnotice("kcn","would have posted needs password and then followed up");
[self startFollowupKitRepair];
}
}

- (void) postKickedOutAlert: (int) reason
{

if(!_isAccountICDP){
NSUserNotificationCenter *noteCenter = appropriateNotificationCenter();
for (NSUserNotification *note in noteCenter.deliveredNotifications) {
if (note.userInfo[(NSString*) kKickedOutKey]) {
if (note.isPresented) {
secnotice("kcn", "Already posted&presented (removing): %@", note);
[appropriateNotificationCenter() removeDeliveredNotification: note];
} else {
secnotice("kcn", "Already posted, but not presented: %@", note);
}
}
}

NSString *message = CFBridgingRelease(SecCopyCKString(SEC_CK_PWD_REQUIRED_BODY_OSX));
if (os_variant_has_internal_ui("iCloudKeychain")) {
static const char *departureReasonStrings[] = {
"kSOSDepartureReasonError",
"kSOSNeverLeftCircle",
"kSOSWithdrewMembership",
"kSOSMembershipRevoked",
"kSOSLeftUntrustedCircle",
"kSOSNeverAppliedToCircle",
"kSOSDiscoveredRetirement",
"kSOSLostPrivateKey",
"unknown reason"
};
int idx = (kSOSDepartureReasonError <= reason && reason <= kSOSLostPrivateKey) ? reason : (kSOSLostPrivateKey + 1);
NSString *reason_str = [NSString stringWithFormat:(__bridge_transfer NSString *) SecCopyCKString(SEC_CK_CR_REASON_INTERNAL), departureReasonStrings[idx]];
message = [message stringByAppendingString: reason_str];
}

// <rdar://problem/21988060> Improve wording of the iCloud keychain drop/reset error messages
// Contrary to HI spec (and I think it makes more sense)
// 1. otherButton == top : Not Now
// 2. actionButton == bottom: Continue
// 3. If we followed HI spec, replace "Activate" => "Dismiss" in note.userInfo below
NSUserNotification *note = [NSUserNotification new];
note.title = (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_PWD_REQUIRED_TITLE);
note.informativeText = message;
note._identityImage = [NSImage bundleImage];
note._identityImageStyle = _NSUserNotificationIdentityImageStyleRectangleNoBorder;
note.otherButtonTitle = (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_NOT_NOW);
note.actionButtonTitle = (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_CONTINUE);
note.identifier = [[NSUUID new] UUIDString];

note.userInfo = @{
kKickedOutKey : @1,
kValidOnlyOutOfCircleKey: @1,
@"Activate" : (__bridge NSString *) kMMPropertyKeychainMRDetailsAEAction,
};

secnotice("kcn", "body=%@", note.informativeText);
secnotice("kcn", "About to post #-/%lu (KICKOUT): %@", noteCenter.deliveredNotifications.count, note);
[appropriateNotificationCenter() deliverNotification:note];
}

else{
secnotice("kcn","postKickedOutAlert starting followup repair");
[self startFollowupKitRepair];
}
}

- (void) postApplicationReminder
{
NSUserNotificationCenter *noteCenter = appropriateNotificationCenter();
for (NSUserNotification *note in noteCenter.deliveredNotifications) {
if (note.userInfo[@"ApplicationReminder"]) {
if (note.isPresented) {
secnotice("kcn", "Already posted&presented (removing): %@", note);
[appropriateNotificationCenter() removeDeliveredNotification: note];
} else {
secnotice("kcn", "Already posted, but not presented: %@", note);
}
}
}

// <rdar://problem/21988060> Improve wording of the iCloud keychain drop/reset error messages
// Contrary to HI spec (and I think it makes more sense)
// 1. otherButton == top : Not Now
// 2. actionButton == bottom: Continue
// 3. If we followed HI spec, replace "Activate" => "Dismiss" in note.userInfo below
NSUserNotification *note = [NSUserNotification new];
note.title = (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_REMINDER_TITLE_OSX);
note.informativeText = (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_REMINDER_BODY_OSX);
note._identityImage = [NSImage bundleImage];
note._identityImageStyle = _NSUserNotificationIdentityImageStyleRectangleNoBorder;
note.otherButtonTitle = (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_NOT_NOW);
note.actionButtonTitle = (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_CONTINUE);
note.identifier = [[NSUUID new] UUIDString];

note.userInfo = @{
@"ApplicationReminder" : @1,
kValidOnlyOutOfCircleKey: @1,
@"Activate" : (__bridge NSString *) kMMPropertyKeychainWADetailsAEAction,
};

secnotice("kcn", "About to post #-/%lu (REMINDER): %@ (I=%@)", noteCenter.deliveredNotifications.count, note, [note.userInfo compactDescription]);
[appropriateNotificationCenter() deliverNotification:note];
}

@end