1/* 2 * Copyright (c) 2013-2014 Apple Inc. All Rights Reserved. 3 * 4 * @APPLE_LICENSE_HEADER_START@ 5 * 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 11 * file. 12 * 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. 20 * 21 * @APPLE_LICENSE_HEADER_END@ 22 */ 23 24 25#import "KNAppDelegate.h" 26#import "KDSecCircle.h" 27#import "KDCirclePeer.h" 28#import "NSDictionary+compactDescription.h" 29#import <AOSUI/NSImageAdditions.h> 30#import <AppleSystemInfo/AppleSystemInfo.h> 31#import <Security/SecFrameworkStrings.h> 32 33#import <AOSAccounts/MobileMePrefsCoreAEPrivate.h> 34#import <AOSAccounts/MobileMePrefsCore.h> 35 36static char *kLaunchLaterXPCName = "com.apple.security.Keychain-Circle-Notification-TICK"; 37static const NSString *kKickedOutKey = @"KickedOut"; 38static const NSString *kValidOnlyOutOfCircleKey = @"ValidOnlyOutOfCircle"; 39 40@implementation KNAppDelegate 41 42static NSUserNotificationCenter *appropriateNotificationCenter() 43{ 44 return [NSUserNotificationCenter _centerForIdentifier:@"com.apple.security.keychain-circle-notification" type:_NSUserNotificationCenterTypeSystem]; 45} 46 47-(void)notifyiCloudPreferencesAbout:(NSString *)eventName; 48{ 49 if (nil == eventName) { 50 return; 51 } 52 53 NSString *account = (__bridge NSString *)(MMCopyLoggedInAccount()); 54 NSLog(@"notifyiCloudPreferencesAbout %@", eventName); 55 56 AEDesc aeDesc; 57 BOOL createdAEDesc = createAEDescWithAEActionAndAccountID((__bridge NSString *)kMMServiceIDKeychainSync, eventName, account, &aeDesc); 58 if (createdAEDesc) 59 { 60 OSErr err; 61 LSLaunchURLSpec lsSpec; 62 63 lsSpec.appURL = NULL; 64 lsSpec.itemURLs = (__bridge CFArrayRef)([NSArray arrayWithObject:[NSURL fileURLWithPath:@"/System/Library/PreferencePanes/iCloudPref.prefPane"]]); 65 lsSpec.passThruParams = &aeDesc; 66 lsSpec.launchFlags = kLSLaunchDefaults | kLSLaunchAsync; 67 lsSpec.asyncRefCon = NULL; 68 69 err = LSOpenFromURLSpec(&lsSpec, NULL); 70 71 if (err) { 72 NSLog(@"Can't send event %@, err=%d", eventName, err); 73 } 74 AEDisposeDesc(&aeDesc); 75 } 76 else 77 { 78 NSLog(@"unable to create and send aedesc for account: '%@' and action: '%@'\n", account, eventName); 79 } 80} 81 82-(void)showiCloudPrefrences 83{ 84 static NSAppleScript *script = nil; 85 if (!script) { 86 script = [[NSAppleScript alloc] initWithSource:@"tell application \"System Preferences\"\n\ 87 activate\n\ 88 set the current pane to pane id \"com.apple.preferences.icloud\"\n\ 89 end tell"]; 90 } 91 92 NSDictionary *appleScriptError = nil; 93 [script executeAndReturnError:&appleScriptError]; 94 95 if (appleScriptError) { 96 NSLog(@"appleScriptError: %@", appleScriptError); 97 } else { 98 NSLog(@"NO appleScript error"); 99 } 100} 101 102-(void)timerCheck 103{ 104 NSDate *nowish = [NSDate new]; 105 self.state = [KNPersistantState loadFromStorage]; 106 if ([nowish compare:self.state.pendingApplicationReminder] != NSOrderedAscending) { 107 NSLog(@"REMINDER TIME: %@ >>> %@", nowish, self.state.pendingApplicationReminder); 108 // self.circle.rawStatus might not be valid yet 109 if (SOSCCThisDeviceIsInCircle(NULL) == kSOSCCRequestPending) { 110 // Still have a request pending, send reminder, and also in addtion to the UI 111 // we need to send a notification for iCloud pref pane to pick up 112 113 CFNotificationCenterPostNotificationWithOptions(CFNotificationCenterGetDistributedCenter(), CFSTR("com.apple.security.secureobjectsync.pendingApplicationReminder"), (__bridge const void *)([self.state.applcationDate description]), NULL, 0); 114 115 [self postApplicationReminder]; 116 self.state.pendingApplicationReminder = [nowish dateByAddingTimeInterval:[self getPendingApplicationReminderInterval]]; 117 [self.state writeToStorage]; 118 } 119 } 120} 121 122-(void)scheduleActivityAt:(NSDate*)time 123{ 124 if ([time compare:[NSDate distantFuture]] != NSOrderedSame) { 125 NSTimeInterval howSoon = [time timeIntervalSinceNow]; 126 if (howSoon > 0) { 127 [self scheduleActivityIn:howSoon]; 128 } else { 129 [self timerCheck]; 130 } 131 } 132} 133 134-(void)scheduleActivityIn:(int)alertInterval 135{ 136 xpc_object_t options = xpc_dictionary_create(NULL, NULL, 0); 137 xpc_dictionary_set_uint64(options, XPC_ACTIVITY_DELAY, alertInterval); 138 xpc_dictionary_set_uint64(options, XPC_ACTIVITY_GRACE_PERIOD, XPC_ACTIVITY_INTERVAL_1_MIN); 139 xpc_dictionary_set_bool(options, XPC_ACTIVITY_REPEATING, false); 140 xpc_dictionary_set_bool(options, XPC_ACTIVITY_ALLOW_BATTERY, true); 141 xpc_dictionary_set_string(options, XPC_ACTIVITY_PRIORITY, XPC_ACTIVITY_PRIORITY_UTILITY); 142 143 xpc_activity_register(kLaunchLaterXPCName, options, ^(xpc_activity_t activity) { 144 [self timerCheck]; 145 }); 146} 147 148-(NSTimeInterval)getPendingApplicationReminderInterval 149{ 150 if (self.state.pendingApplicationReminderInterval) { 151 return [self.state.pendingApplicationReminderInterval doubleValue]; 152 } else { 153 return 48*24*60*60; 154 } 155} 156 157- (void)applicationDidFinishLaunching:(NSNotification *)aNotification 158{ 159 appropriateNotificationCenter().delegate = self; 160 161 NSLog(@"Posted at launch: %@", appropriateNotificationCenter().deliveredNotifications); 162 163 self.viewedIds = [NSMutableSet new]; 164 self.circle = [KDSecCircle new]; 165 self.state = [KNPersistantState loadFromStorage]; 166 KNAppDelegate *me = self; 167 168 [self.circle addChangeCallback:^{ 169 me.state = [KNPersistantState loadFromStorage]; 170 if ((me.state.lastCircleStatus == kSOSCCInCircle && !me.circle.isInCircle) || me.state.debugLeftReason) { 171 enum DepartureReason reason = kSOSNeverLeftCircle; 172 if (me.state.debugLeftReason) { 173 reason = [me.state.debugLeftReason intValue]; 174 me.state.debugLeftReason = nil; 175 } else { 176 CFErrorRef err = NULL; 177 reason = SOSCCGetLastDepartureReason(&err); 178 if (reason == kSOSDepartureReasonError) { 179 NSLog(@"SOSCCGetLastDepartureReason err: %@", err); 180 } 181 } 182 183 //NSString *model = (__bridge NSString *)(ASI_CopyComputerModelName(FALSE)); 184 NSString *body = nil; 185 switch (reason) { 186 case kSOSDepartureReasonError: 187 case kSOSNeverLeftCircle: 188 case kSOSWithdrewMembership: 189 break; 190 191 default: 192 NSLog(@"Unknown departure reason %d", reason); 193 // fallthrough on purpose 194 195 case kSOSMembershipRevoked: 196 case kSOSLeftUntrustedCircle: 197 body = NSLocalizedString(@"Approve this Mac from another device to use iCloud Keychain.", @"Body for iCloud Keychain Reset notification"); 198 break; 199 } 200 [me.state writeToStorage]; 201 NSLog(@"departure reason %d, body=%@", reason, body); 202 if (body) { 203 [me postKickedOutWithMessage: body]; 204 } 205 } else if (me.circle.isInCircle) { 206 // We are in a circle, so we should get rid of any reset notifications that are hanging out 207 NSUserNotificationCenter *noteCenter = appropriateNotificationCenter(); 208 for (NSUserNotification *note in noteCenter.deliveredNotifications) { 209 if (note.userInfo[kValidOnlyOutOfCircleKey]) { 210 NSLog(@"Removing existing notification (%@) now that we are in circle", note); 211 [appropriateNotificationCenter() removeDeliveredNotification: note]; 212 } 213 } 214 } 215 216 [me timerCheck]; 217 218 if (me.state.lastCircleStatus != kSOSCCRequestPending && me.circle.rawStatus == kSOSCCRequestPending) { 219 NSLog(@"Entered RequestPending"); 220 NSDate *nowish = [NSDate new]; 221 me.state.applcationDate = nowish; 222 me.state.pendingApplicationReminder = [me.state.applcationDate dateByAddingTimeInterval:[me getPendingApplicationReminderInterval]]; 223 [me.state writeToStorage]; 224 [me scheduleActivityAt:me.state.pendingApplicationReminder]; 225 } 226 227 NSMutableSet *applicantIds = [NSMutableSet new]; 228 for (KDCirclePeer *applicant in me.circle.applicants) { 229 if (!me.circle.isInCircle) { 230 // We don't want to yammer on about circles we aren't in, 231 // and we don't want to be extra confusing announcing our 232 // own join requests as if the user could approve them 233 // locally! 234 break; 235 } 236 [me postForApplicant:applicant]; 237 [applicantIds addObject:applicant.idString]; 238 } 239 240 NSUserNotificationCenter *notificationCenter = appropriateNotificationCenter(); 241 NSLog(@"Checking validity of %lu notes", (unsigned long)notificationCenter.deliveredNotifications.count); 242 for (NSUserNotification *note in notificationCenter.deliveredNotifications) { 243 if (note.userInfo[@"applicantId"] && ![applicantIds containsObject:note.userInfo[@"applicantId"]]) { 244 NSLog(@"No longer an applicant (%@) for %@ (I=%@)", note.userInfo[@"applicantId"], note, [note.userInfo compactDescription]); 245 [notificationCenter removeDeliveredNotification:note]; 246 } else { 247 NSLog(@"Still an applicant (%@) for %@ (I=%@)", note.userInfo[@"applicantId"], note, [note.userInfo compactDescription]); 248 } 249 } 250 251 me.state.lastCircleStatus = me.circle.rawStatus; 252 253 [me.state writeToStorage]; 254 }]; 255 256 [me scheduleActivityAt:me.state.pendingApplicationReminder]; 257} 258 259-(BOOL)userNotificationCenter:(NSUserNotificationCenter *)center shouldPresentNotification:(NSUserNotification *)notification 260{ 261 return YES; 262} 263 264-(void)userNotificationCenter:(NSUserNotificationCenter *)center didActivateNotification:(NSUserNotification *)notification 265{ 266 if (notification.activationType == NSUserNotificationActivationTypeActionButtonClicked) { 267 [self notifyiCloudPreferencesAbout:notification.userInfo[@"Activate"]]; 268 } 269 270 // The "Later" seems handled Ok without doing anything here, but KickedOut & other special items need an action 271 if (notification.userInfo[@"SPECIAL"]) { 272 NSLog(@"ACTIVATED (remove): %@", notification); 273 [appropriateNotificationCenter() removeDeliveredNotification:notification]; 274 } else { 275 NSLog(@"ACTIVATED (NOT removed): %@", notification); 276 } 277} 278 279-(void)userNotificationCenter:(NSUserNotificationCenter *)center didDismissAlert:(NSUserNotification *)notification 280{ 281 [self notifyiCloudPreferencesAbout:notification.userInfo[@"Dismiss"]]; 282 283 if (!notification.userInfo[@"SPECIAL"]) { 284 // If we don't do anything here & another notification comes in we 285 // will repost the alert, which will be dumb. 286 id applicantId = notification.userInfo[@"applicantId"]; 287 if (applicantId != nil) { 288 [self.viewedIds addObject:applicantId]; 289 } 290 NSLog(@"DISMISS (t) %@", notification); 291 } else { 292 NSLog(@"DISMISS (f) %@", notification); 293 [appropriateNotificationCenter() removeDeliveredNotification:notification]; 294 } 295} 296 297-(void)postForApplicant:(KDCirclePeer*)applicant 298{ 299 static int postCount = 0; 300 301 if ([self.viewedIds containsObject:applicant.idString]) { 302 NSLog(@"Already viewed %@, skipping", applicant); 303 return; 304 } 305 306 NSUserNotificationCenter *noteCenter = appropriateNotificationCenter(); 307 for (NSUserNotification *note in noteCenter.deliveredNotifications) { 308 if ([applicant.idString isEqualToString:note.userInfo[@"applicantId"]]) { 309 if (note.isPresented) { 310 NSLog(@"Already posted&presented: %@ (I=%@)", note, note.userInfo); 311 return; 312 } else { 313 NSLog(@"Already posted, but not presented: %@ (I=%@)", note, note.userInfo); 314 } 315 } 316 } 317 318 NSUserNotification *note = [NSUserNotification new]; 319 320 // Genstrings command line is: genstrings -o en.lproj -u KNAppDelegate.m 321 note.title = [NSString stringWithFormat:NSLocalizedString(@"iCloud Keychain", @"Title for new keychain syncing device notification")]; 322 note.informativeText = [NSString stringWithFormat:NSLocalizedString(@"\\U201C%1$@\\U201D wants to use your passwords.", @"Message text for new keychain syncing device notification"), applicant.name]; 323 324 note.hasActionButton = YES; 325 note._displayStyle = _NSUserNotificationDisplayStyleAlert; 326 note._identityImage = [NSImage bundleImage]; 327 note._identityImageHasBorder = NO; 328 note._actionButtonIsSnooze = YES; 329 note.actionButtonTitle = NSLocalizedString(@"Later", @"Button label to dismiss device notification"); 330 note.otherButtonTitle = NSLocalizedString(@"View", @"Button label to view device notification"); 331 332 note.identifier = [[NSUUID new] UUIDString]; 333 334 note.userInfo = @{@"applicantName": applicant.name, 335 @"applicantId": applicant.idString, 336 @"Dismiss": (__bridge NSString *)kMMPropertyKeychainAADetailsAEAction, 337 }; 338 339 NSLog(@"About to post#%d/%lu (%@): %@", postCount, (unsigned long)noteCenter.deliveredNotifications.count, applicant.idString, note); 340 [appropriateNotificationCenter() deliverNotification:note]; 341 342 postCount++; 343} 344 345-(void)postKickedOutWithMessage:(NSString*)body 346{ 347 NSUserNotificationCenter *noteCenter = appropriateNotificationCenter(); 348 for (NSUserNotification *note in noteCenter.deliveredNotifications) { 349 if (note.userInfo[kKickedOutKey]) { 350 if (note.isPresented) { 351 NSLog(@"Already posted&presented (removing): %@", note); 352 [appropriateNotificationCenter() removeDeliveredNotification: note]; 353 } else { 354 NSLog(@"Already posted, but not presented: %@", note); 355 } 356 } 357 } 358 359 NSUserNotification *note = [NSUserNotification new]; 360 361 note.title = NSLocalizedString(@"iCloud Keychain Was Reset", @"Title for iCloud Keychain Reset notification"); 362 note.informativeText = body; // Already LOCed 363 364 note._identityImage = [NSImage bundleImage]; 365 note._identityImageHasBorder = NO; 366 note.otherButtonTitle = NSLocalizedString(@"Close", @"Close button"); 367 note.actionButtonTitle = NSLocalizedString(@"Options", @"Options Button"); 368 369 note.identifier = [[NSUUID new] UUIDString]; 370 371 note.userInfo = @{kKickedOutKey: @1, 372 kValidOnlyOutOfCircleKey: @1, 373 @"SPECIAL": @1, 374 @"Activate": (__bridge NSString *)kMMPropertyKeychainMRDetailsAEAction, 375 }; 376 377 NSLog(@"About to post#-/%lu (KICKOUT): %@", (unsigned long)noteCenter.deliveredNotifications.count, note); 378 [appropriateNotificationCenter() deliverNotification:note]; 379} 380 381-(void)postApplicationReminder 382{ 383 NSUserNotificationCenter *noteCenter = appropriateNotificationCenter(); 384 for (NSUserNotification *note in noteCenter.deliveredNotifications) { 385 if (note.userInfo[@"ApplicationReminder"]) { 386 if (note.isPresented) { 387 NSLog(@"Already posted&presented (removing): %@", note); 388 [appropriateNotificationCenter() removeDeliveredNotification: note]; 389 } else { 390 NSLog(@"Already posted, but not presented: %@", note); 391 } 392 } 393 } 394 395 NSUserNotification *note = [NSUserNotification new]; 396 397 note.title = NSLocalizedString(@"iCloud Keychain", @"Title for iCloud Keychain Application still pending (from this device) reminder"); 398 note.informativeText = NSLocalizedString(@"Approve this Mac from another device to use iCloud Keychain.", @"Body text for iCloud Keychain Application still pending (from this device) reminder"); 399 400 note._identityImage = [NSImage bundleImage]; 401 note._identityImageHasBorder = NO; 402 note.otherButtonTitle = NSLocalizedString(@"Close", @"Close button"); 403 note.actionButtonTitle = NSLocalizedString(@"Options", @"Options Button"); 404 405 note.identifier = [[NSUUID new] UUIDString]; 406 407 note.userInfo = @{@"ApplicationReminder": @1, 408 kValidOnlyOutOfCircleKey: @1, 409 @"SPECIAL": @1, 410 @"Activate": (__bridge NSString *)kMMPropertyKeychainWADetailsAEAction, 411 }; 412 413 NSLog(@"About to post#-/%lu (REMINDER): %@ (I=%@)", (unsigned long)noteCenter.deliveredNotifications.count, note, [note.userInfo compactDescription]); 414 [appropriateNotificationCenter() deliverNotification:note]; 415} 416 417@end 418