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