]> git.saurik.com Git - apple/security.git/blob - CircleJoinRequested/CircleJoinRequested.m
d6942e5708b3cdb265d8a701e19a35ac8d319598
[apple/security.git] / CircleJoinRequested / CircleJoinRequested.m
1 /*
2 * Copyright (c) 2013-2017 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 #import <Accounts/Accounts.h>
25 #import <Accounts/ACAccountStore_Private.h>
26 #import <Accounts/ACAccountType_Private.h>
27 #import <AggregateDictionary/ADClient.h>
28 #import <AppSupport/AppSupportUtils.h>
29 #import <AppleAccount/AppleAccount.h>
30 #import <AppleAccount/ACAccountStore+AppleAccount.h>
31 #import <CloudServices/SecureBackup.h>
32 #import <CoreFoundation/CFUserNotification.h>
33 #import <Foundation/Foundation.h>
34 #import <ManagedConfiguration/MCProfileConnection.h>
35 #import <ManagedConfiguration/MCFeatures.h>
36 #import <MobileCoreServices/MobileCoreServices.h>
37 #import <MobileCoreServices/LSApplicationWorkspace.h>
38 #import <MobileGestalt.h>
39 #import <ProtectedCloudStorage/CloudIdentity.h>
40 #import <Security/SecFrameworkStrings.h>
41 #import <SpringBoardServices/SBSCFUserNotificationKeys.h>
42 #include <dispatch/dispatch.h>
43 #include "SecureObjectSync/SOSCloudCircle.h"
44 #include "SecureObjectSync/SOSCloudCircleInternal.h"
45 #include "SecureObjectSync/SOSPeerInfo.h"
46 #include "SecureObjectSync/SOSInternal.h"
47 #include <notify.h>
48 #include <sysexits.h>
49 #import "Applicant.h"
50 #import "NSArray+map.h"
51 #import "PersistentState.h"
52 #include <xpc/private.h>
53 #include <sys/time.h>
54 #import "NSDate+TimeIntervalDescription.h"
55 #include <xpc/activity.h>
56 #include <xpc/private.h>
57 #import "os/activity.h"
58 #import <syslog.h>
59 #include "utilities/SecCFRelease.h"
60 #include "utilities/debugging.h"
61 #include "utilities/SecAKSWrappers.h"
62 #include "utilities/SecCFWrappers.h"
63
64 #import "CoreCDP/CDPFollowUpController.h"
65 #import "CoreCDP/CDPFollowUpContext.h"
66 #import <CoreCDP/CDPAccount.h>
67
68 // As long as we are logging the failure use exit code of zero to make launchd happy
69 #define EXIT_LOGGED_FAILURE(code) xpc_transaction_end(); exit(0)
70
71 const char *kLaunchLaterXPCName = "com.apple.security.CircleJoinRequestedTick";
72 CFRunLoopSourceRef currentAlertSource = NULL;
73 CFUserNotificationRef currentAlert = NULL;
74 bool currentAlertIsForApplicants = true;
75 bool currentAlertIsForKickOut = false;
76 NSMutableDictionary *applicants = nil;
77 volatile NSString *debugState = @"main?";
78 dispatch_block_t doOnceInMainBlockChain = NULL;
79 bool _isLocked = true;
80 bool processApplicantsAfterUnlock = false;
81 bool _unlockedSinceBoot = false;
82 bool _hasPostedFollowupAndStillInError = false;
83 bool _isAccountICDP = false;
84
85
86 NSString *castleKeychainUrl = @"prefs:root=APPLE_ACCOUNT&path=ICLOUD_SERVICE/com.apple.Dataclass.KeychainSync/ADVANCED";
87 NSString *rejoinICDPUrl = @"prefs:root=APPLE_ACCOUNT&aaaction=CDP&command=rejoin";
88
89 BOOL processRequests(CFErrorRef *error);
90
91 static void PSKeychainSyncIsUsingICDP(void)
92 {
93 ACAccountStore *accountStore = [[ACAccountStore alloc] init];
94 ACAccount *account = [accountStore aa_primaryAppleAccount];
95 NSString *dsid = account.accountProperties[@"personID"];
96 BOOL isICDPEnabled = NO;
97 if (dsid) {
98 isICDPEnabled = [CDPAccount isICDPEnabledForDSID:dsid];
99 NSLog(@"iCDP: PSKeychainSyncIsUsingICDP returning %@", isICDPEnabled ? @"TRUE" : @"FALSE");
100 } else {
101 NSLog(@"iCDP: no primary account");
102 }
103
104 _isAccountICDP = isICDPEnabled;
105 secnotice("cjr", "account is icdp: %d", _isAccountICDP);
106 }
107
108 static void keybagDidLock()
109 {
110 secnotice("cjr", "keybagDidLock");
111 }
112
113 static void keybagDidUnlock()
114 {
115 secnotice("cjr", "keybagDidUnlock");
116
117 CFErrorRef error = NULL;
118
119 if(processApplicantsAfterUnlock){
120 processRequests(&error);
121 processApplicantsAfterUnlock = false;
122 }
123
124 SOSCCStatus circleStatus = SOSCCThisDeviceIsInCircle(&error);
125 if(_isAccountICDP && (circleStatus == kSOSCCError || circleStatus == kSOSCCCircleAbsent || circleStatus == kSOSCCNotInCircle) && _hasPostedFollowupAndStillInError == false){
126 NSError *localError = nil;
127 CDPFollowUpContext *context = [CDPFollowUpContext contextForStateRepair];
128 CDPFollowUpController *cdpd = [[CDPFollowUpController alloc] init];
129 [cdpd postFollowUpWithContext:context error:&localError ];
130 secnotice("cjr", "account is icdp");
131 if(localError){
132 secnotice("cjr", "request to CoreCDP to follow up failed: %@", localError);
133 }
134 else{
135 secnotice("cjr", "CoreCDP handling follow up");
136 _hasPostedFollowupAndStillInError = true;
137 }
138 }
139 else if(_isAccountICDP && circleStatus == kSOSCCInCircle){
140 _hasPostedFollowupAndStillInError = false;
141 }
142 else{
143 secnotice("cjr", "account not icdp");
144 }
145 }
146
147 static bool updateIsLocked ()
148 {
149 CFErrorRef aksError = NULL;
150 if (!SecAKSGetIsLocked(&_isLocked, &aksError)) {
151 _isLocked = YES;
152 secerror("Got error querying lock state: %@", aksError);
153 CFReleaseSafe(aksError);
154 return NO;
155 }
156 if (!_isLocked)
157 _unlockedSinceBoot = YES;
158 return YES;
159 }
160
161 static void keybagStateChange ()
162 {
163 secerror("osactivity initiated");
164 os_activity_initiate("keybagStateChanged", OS_ACTIVITY_FLAG_DEFAULT, ^{
165 BOOL wasLocked = _isLocked;
166 if ( updateIsLocked()) {
167 if (wasLocked == _isLocked)
168 secerror("still %s ignoring", _isLocked ? "locked" : "unlocked");
169 else if (_isLocked)
170 keybagDidLock();
171 else
172 keybagDidUnlock();
173 }
174 });
175 }
176
177 static void doOnceInMain(dispatch_block_t block)
178 {
179 if (doOnceInMainBlockChain) {
180 doOnceInMainBlockChain = ^{
181 doOnceInMainBlockChain();
182 block();
183 };
184 } else {
185 doOnceInMainBlockChain = block;
186 }
187 }
188
189
190 static NSString *appleIDAccountName()
191 {
192 ACAccountStore *accountStore = [[ACAccountStore alloc] init];
193 ACAccount *primaryAppleAccount = [accountStore aa_primaryAppleAccount];
194 return primaryAppleAccount.username;
195 }
196
197
198 static CFOptionFlags flagsForAsk(Applicant *applicant)
199 {
200 return kCFUserNotificationPlainAlertLevel | CFUserNotificationSecureTextField(0);
201 }
202
203
204 // NOTE: gives precedence to OnScreen
205 static Applicant *firstApplicantWaitingOrOnScreen()
206 {
207 Applicant *waiting = nil;
208 for (Applicant *applicant in [applicants objectEnumerator]) {
209 if (applicant.applicantUIState == ApplicantOnScreen) {
210 return applicant;
211 } else if (applicant.applicantUIState == ApplicantWaiting) {
212 waiting = applicant;
213 }
214 }
215
216 return waiting;
217 }
218
219
220 static NSMutableArray *applicantsInState(ApplicantUIState state)
221 {
222 NSMutableArray *results = [NSMutableArray new];
223 for (Applicant *applicant in [applicants objectEnumerator]) {
224 if (applicant.applicantUIState == state) {
225 [results addObject:applicant];
226 }
227 }
228
229 return results;
230 }
231
232
233 BOOL processRequests(CFErrorRef *error) {
234 NSMutableArray *toAccept = [[applicantsInState(ApplicantAccepted) mapWithBlock:^id(id obj) {return (id)[obj rawPeerInfo];}] mutableCopy];
235 NSMutableArray *toReject = [[applicantsInState(ApplicantRejected) mapWithBlock:^id(id obj) {return (id)[obj rawPeerInfo];}] mutableCopy];
236 bool ok = true;
237
238 if ([toAccept count]) {
239 secnotice("cjr", "Process accept: %@", toAccept);
240 ok = ok && SOSCCAcceptApplicants((__bridge CFArrayRef) toAccept, error);
241 if (ok) {
242 secnotice("cjr", "kSOSCCHoldLockForInitialSync");
243 notify_post(kSOSCCHoldLockForInitialSync);
244 }
245 }
246
247 if ([toReject count]) {
248 secnotice("cjr", "Process reject: %@", toReject);
249 ok = ok && SOSCCRejectApplicants((__bridge CFArrayRef) toReject, error);
250 }
251
252 return ok;
253 }
254
255
256 static void cancelCurrentAlert(bool stopRunLoop) {
257 if (currentAlertSource) {
258 CFRunLoopRemoveSource(CFRunLoopGetCurrent(), currentAlertSource, kCFRunLoopDefaultMode);
259 CFReleaseNull(currentAlertSource);
260 }
261 if (currentAlert) {
262 CFUserNotificationCancel(currentAlert);
263 CFReleaseNull(currentAlert);
264 }
265 if (stopRunLoop) {
266 CFRunLoopStop(CFRunLoopGetCurrent());
267 }
268 currentAlertIsForKickOut = currentAlertIsForApplicants = false;
269 }
270
271
272 static void askAboutAll(bool passwordFailure);
273
274
275 static void applicantChoice(CFUserNotificationRef userNotification, CFOptionFlags responseFlags)
276 {
277 ApplicantUIState choice;
278
279 if (kCFUserNotificationAlternateResponse == responseFlags) {
280 choice = ApplicantRejected;
281 } else if (kCFUserNotificationDefaultResponse == responseFlags) {
282 choice = ApplicantAccepted;
283 } else {
284 secnotice("cjr", "Unexpected response %lu", responseFlags);
285 choice = ApplicantRejected;
286 }
287
288 BOOL processed = NO;
289 CFErrorRef error = NULL;
290 NSArray *onScreen = applicantsInState(ApplicantOnScreen);
291
292 [onScreen enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
293 Applicant* applicant = (Applicant *) obj;
294 applicant.applicantUIState = choice;
295 }];
296
297 if (choice == ApplicantRejected) {
298 // If this device has ever set up the public key this should work without the password...
299 processed = processRequests(&error);
300 if (processed) {
301 secnotice("cjr", "Didn't need password to process %@", onScreen);
302 cancelCurrentAlert(true);
303 return;
304 } else {
305 // ...however if the public key gets lost we should "just" fall through to the validate
306 // password path.
307 secnotice("cjr", "Couldn't process reject without password (e=%@) for %@ (will try with password next)", error, onScreen);
308
309 if(CFErrorIsMalfunctioningKeybagError(error)){
310 secnotice("cjr", "system is locked, dismiss the notification");
311 processApplicantsAfterUnlock = true;
312 return;
313 }
314 }
315 CFReleaseNull(error);
316 }
317
318 NSString *password = (__bridge NSString *) CFUserNotificationGetResponseValue(userNotification, kCFUserNotificationTextFieldValuesKey, 0);
319 if (!password) {
320 secnotice("cjr", "No password given, retry");
321 askAboutAll(true);
322 return;
323 }
324 const char *passwordUTF8 = [password UTF8String];
325 NSData *passwordBytes = [NSData dataWithBytes:passwordUTF8 length:strlen(passwordUTF8)];
326
327 // Sometimes securityd crashes between SOSCCRegisterUserCredentials and processRequests
328 // (which results in a process error -- I think this is 13355140), as a workaround we retry
329 // failure a few times before we give up.
330 for (int try = 0; try < 5 && !processed; try++) {
331 if (!SOSCCTryUserCredentials(CFSTR(""), (__bridge CFDataRef) passwordBytes, &error)) {
332 secnotice("cjr", "Try user credentials failed %@", error);
333 if ((error == NULL) ||
334 (CFEqual(kSOSErrorDomain, CFErrorGetDomain(error)) && kSOSErrorWrongPassword == CFErrorGetCode(error))) {
335 secnotice("cjr", "Calling askAboutAll again...");
336 [onScreen enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
337 Applicant *applicant = (Applicant*) obj;
338 applicant.applicantUIState = ApplicantWaiting;
339 }];
340 askAboutAll(true);
341 CFReleaseNull(error);
342 return;
343 }
344 EXIT_LOGGED_FAILURE(EX_DATAERR);
345 }
346
347 processed = processRequests(&error);
348 if (!processed) {
349 secnotice("cjr", "Can't processRequests: %@ for %@", error, onScreen);
350 }
351 CFReleaseNull(error);
352 }
353
354 if (processed && firstApplicantWaitingOrOnScreen()) {
355 cancelCurrentAlert(false);
356 askAboutAll(false);
357 } else {
358 cancelCurrentAlert(true);
359 }
360 }
361
362
363 static void passwordFailurePrompt()
364 {
365 #pragma clang diagnostic push
366 #pragma clang diagnostic ignored "-Wformat-nonliteral"
367 NSString *pwIncorrect = [NSString stringWithFormat:(NSString *)CFBridgingRelease(SecCopyCKString(SEC_CK_PASSWORD_INCORRECT)), appleIDAccountName()];
368 #pragma clang diagnostic pop
369 NSString *tryAgain = CFBridgingRelease(SecCopyCKString(SEC_CK_TRY_AGAIN));
370 NSDictionary *noteAttributes = @{
371 (id) kCFUserNotificationAlertHeaderKey : pwIncorrect,
372 (id) kCFUserNotificationDefaultButtonTitleKey : tryAgain,
373 (id) kCFUserNotificationAlertTopMostKey : @YES, // get us onto the lock screen
374 (__bridge id) SBUserNotificationDontDismissOnUnlock: @YES,
375 (__bridge id) SBUserNotificationDismissOnLock : @NO,
376 };
377 CFOptionFlags flags = kCFUserNotificationPlainAlertLevel;
378 SInt32 err;
379 CFUserNotificationRef note = CFUserNotificationCreate(NULL, 0.0, flags, &err, (__bridge CFDictionaryRef) noteAttributes);
380
381 if (note) {
382 CFUserNotificationReceiveResponse(note, 0.0, &flags);
383 CFRelease(note);
384 }
385 }
386
387
388 static NSString *getLocalizedApprovalBody(NSString *deviceType) {
389 CFStringRef applicationReminder = NULL;
390
391 if ([deviceType isEqualToString:@"iPhone"])
392 applicationReminder = SecCopyCKString(SEC_CK_APPROVAL_BODY_IOS_IPHONE);
393 else if ([deviceType isEqualToString:@"iPod"])
394 applicationReminder = SecCopyCKString(SEC_CK_APPROVAL_BODY_IOS_IPOD);
395 else if ([deviceType isEqualToString:@"iPad"])
396 applicationReminder = SecCopyCKString(SEC_CK_APPROVAL_BODY_IOS_IPAD);
397 else if ([deviceType isEqualToString:@"Mac"])
398 applicationReminder = SecCopyCKString(SEC_CK_APPROVAL_BODY_IOS_MAC);
399 else
400 applicationReminder = SecCopyCKString(SEC_CK_APPROVAL_BODY_IOS_GENERIC);
401
402 return (__bridge_transfer NSString *) applicationReminder;
403 }
404
405
406 static NSDictionary *createNote(Applicant *applicantToAskAbout)
407 {
408 if(!applicantToAskAbout || !applicantToAskAbout.name || !applicantToAskAbout.deviceType)
409 return NULL;
410
411 #pragma clang diagnostic push
412 #pragma clang diagnostic ignored "-Wformat-nonliteral"
413 NSString *header = [NSString stringWithFormat: (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_APPROVAL_TITLE), applicantToAskAbout.name];
414 NSString *body = [NSString stringWithFormat: getLocalizedApprovalBody(applicantToAskAbout.deviceType), appleIDAccountName()];
415 #pragma clang diagnostic pop
416
417 return @{
418 (id) kCFUserNotificationAlertHeaderKey : header,
419 (id) kCFUserNotificationAlertMessageKey : body,
420 (id) kCFUserNotificationDefaultButtonTitleKey : (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_APPROVE),
421 (id) kCFUserNotificationAlternateButtonTitleKey: (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_DECLINE),
422 (id) kCFUserNotificationTextFieldTitlesKey : (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_ICLOUD_PASSWORD),
423 (id) kCFUserNotificationAlertTopMostKey : @YES, // get us onto the lock screen
424 (__bridge_transfer id) SBUserNotificationDontDismissOnUnlock: @YES,
425 (__bridge_transfer id) SBUserNotificationDismissOnLock : @NO,
426 };
427 }
428
429
430 static void askAboutAll(bool passwordFailure)
431 {
432 if ([[MCProfileConnection sharedConnection] effectiveBoolValueForSetting: MCFeatureAccountModificationAllowed] == MCRestrictedBoolExplicitNo) {
433 secnotice("cjr", "Account modifications not allowed.");
434 return;
435 }
436
437 if (passwordFailure) {
438 passwordFailurePrompt();
439 }
440
441 if ((passwordFailure || !currentAlertIsForApplicants) && currentAlert) {
442 if (!currentAlertIsForApplicants) {
443 CFUserNotificationCancel(currentAlert);
444 }
445 // after password failure we need to remove the existing alert and supporting objects
446 // because we can't reuse them.
447 CFReleaseNull(currentAlert);
448 CFReleaseNull(currentAlertSource);
449 }
450 currentAlertIsForApplicants = true;
451
452 Applicant *applicantToAskAbout = firstApplicantWaitingOrOnScreen();
453 secnotice("cjr", "Asking about: %@ (of: %@)", applicantToAskAbout, applicants);
454
455 NSDictionary *noteAttributes = createNote(applicantToAskAbout);
456 if(!noteAttributes) {
457 secnotice("cjr", "NULL data for %@", applicantToAskAbout);
458 cancelCurrentAlert(true);
459 return;
460 }
461
462 CFOptionFlags flags = flagsForAsk(applicantToAskAbout);
463
464 if (currentAlert) {
465 SInt32 err = CFUserNotificationUpdate(currentAlert, 0, flags, (__bridge CFDictionaryRef) noteAttributes);
466 if (err) {
467 secnotice("cjr", "CFUserNotificationUpdate err=%d", (int)err);
468 EXIT_LOGGED_FAILURE(EX_SOFTWARE);
469 }
470 } else {
471 SInt32 err = 0;
472 currentAlert = CFUserNotificationCreate(NULL, 0.0, flags, &err, (__bridge CFDictionaryRef) noteAttributes);
473 if (err) {
474 secnotice("cjr", "Can't make notification for %@ err=%x", applicantToAskAbout, (int)err);
475 EXIT_LOGGED_FAILURE(EX_SOFTWARE);
476 }
477
478 currentAlertSource = CFUserNotificationCreateRunLoopSource(NULL, currentAlert, applicantChoice, 0);
479 CFRunLoopAddSource(CFRunLoopGetCurrent(), currentAlertSource, kCFRunLoopDefaultMode);
480 }
481
482 applicantToAskAbout.applicantUIState = ApplicantOnScreen;
483 }
484
485
486 static void scheduleActivity(int alertInterval)
487 {
488 xpc_object_t options = xpc_dictionary_create(NULL, NULL, 0);
489 xpc_dictionary_set_uint64(options, XPC_ACTIVITY_DELAY, alertInterval);
490 xpc_dictionary_set_uint64(options, XPC_ACTIVITY_GRACE_PERIOD, XPC_ACTIVITY_INTERVAL_1_MIN);
491 xpc_dictionary_set_bool(options, XPC_ACTIVITY_REPEATING, false);
492 xpc_dictionary_set_bool(options, XPC_ACTIVITY_ALLOW_BATTERY, true);
493 xpc_dictionary_set_string(options, XPC_ACTIVITY_PRIORITY, XPC_ACTIVITY_PRIORITY_UTILITY);
494
495 xpc_activity_register(kLaunchLaterXPCName, options, ^(xpc_activity_t activity) {
496 secnotice("cjr", "activity handler fired");
497 });
498 }
499
500
501 static void reminderChoice(CFUserNotificationRef userNotification, CFOptionFlags responseFlags) {
502 if (responseFlags == kCFUserNotificationAlternateResponse || responseFlags == kCFUserNotificationDefaultResponse) {
503 PersistentState *state = [PersistentState loadFromStorage];
504 NSDate *nowish = [NSDate new];
505 state.pendingApplicationReminder = [nowish dateByAddingTimeInterval: state.pendingApplicationReminderAlertInterval];
506 scheduleActivity(state.pendingApplicationReminderAlertInterval);
507 [state writeToStorage];
508 if (responseFlags == kCFUserNotificationAlternateResponse) {
509 // Use security code
510 BOOL ok = [[LSApplicationWorkspace defaultWorkspace] openSensitiveURL:[NSURL URLWithString:castleKeychainUrl] withOptions:nil];
511 secnotice("cjr", "%s iCSC: opening %@ ok=%d", __FUNCTION__, castleKeychainUrl, ok);
512 }
513 }
514
515 cancelCurrentAlert(true);
516 }
517
518
519 static bool iCloudResetAvailable() {
520 SecureBackup *backupd = [SecureBackup new];
521 NSDictionary *backupdResults;
522 NSError *error = [backupd getAccountInfoWithInfo:nil results:&backupdResults];
523 secnotice("cjr", "SecureBackup e=%@ r=%@", error, backupdResults);
524 return (error == nil && [backupdResults[kSecureBackupIsEnabledKey] isEqualToNumber:@YES]);
525 }
526
527
528 static NSString *getLocalizedApplicationReminder() {
529 CFStringRef applicationReminder = NULL;
530 switch (MGGetSInt32Answer(kMGQDeviceClassNumber, MGDeviceClassInvalid)) {
531 case MGDeviceClassiPhone:
532 applicationReminder = SecCopyCKString(SEC_CK_REMINDER_BODY_IOS_IPHONE);
533 break;
534 case MGDeviceClassiPod:
535 applicationReminder = SecCopyCKString(SEC_CK_REMINDER_BODY_IOS_IPOD);
536 break;
537 case MGDeviceClassiPad:
538 applicationReminder = SecCopyCKString(SEC_CK_REMINDER_BODY_IOS_IPAD);
539 break;
540 default:
541 applicationReminder = SecCopyCKString(SEC_CK_REMINDER_BODY_IOS_GENERIC);
542 break;
543 }
544 return (__bridge_transfer NSString *) applicationReminder;
545 }
546
547
548 static void postApplicationReminderAlert(NSDate *nowish, PersistentState *state, unsigned int alertInterval)
549 {
550 NSString *body = getLocalizedApplicationReminder();
551 bool has_iCSC = iCloudResetAvailable();
552
553 if (CPIsInternalDevice() &&
554 state.defaultPendingApplicationReminderAlertInterval != state.pendingApplicationReminderAlertInterval) {
555 #ifdef DEBUG
556 body = [body stringByAppendingFormat: @"〖debug interval %u; wait time %@〗",
557 state.pendingApplicationReminderAlertInterval,
558 [nowish copyDescriptionOfIntervalSince:state.applicationDate]];
559 #endif
560 }
561
562 NSDictionary *pendingAttributes = @{
563 (id) kCFUserNotificationAlertHeaderKey : CFBridgingRelease(SecCopyCKString(SEC_CK_REMINDER_TITLE_IOS)),
564 (id) kCFUserNotificationAlertMessageKey : body,
565 (id) kCFUserNotificationDefaultButtonTitleKey : CFBridgingRelease(SecCopyCKString(SEC_CK_REMINDER_BUTTON_OK)),
566 (id) kCFUserNotificationAlternateButtonTitleKey: has_iCSC ? CFBridgingRelease(SecCopyCKString(SEC_CK_REMINDER_BUTTON_ICSC)) : @"",
567 (id) kCFUserNotificationAlertTopMostKey : @YES,
568 (__bridge id) SBUserNotificationDontDismissOnUnlock : @YES,
569 (__bridge id) SBUserNotificationDismissOnLock : @NO,
570 };
571 SInt32 err = 0;
572 currentAlert = CFUserNotificationCreate(NULL, 0.0, kCFUserNotificationPlainAlertLevel, &err, (__bridge CFDictionaryRef) pendingAttributes);
573
574 if (err) {
575 secnotice("cjr", "Can't make pending notification err=%x", (int)err);
576 } else {
577 currentAlertIsForApplicants = false;
578 currentAlertSource = CFUserNotificationCreateRunLoopSource(NULL, currentAlert, reminderChoice, 0);
579 CFRunLoopAddSource(CFRunLoopGetCurrent(), currentAlertSource, kCFRunLoopDefaultMode);
580 }
581 }
582
583
584 static void kickOutChoice(CFUserNotificationRef userNotification, CFOptionFlags responseFlags) {
585 secnotice("cjr", "kOC %@ %lu", userNotification, responseFlags);
586
587 //default response: continue -> settings pref pane advanced keychain sync page
588 if (responseFlags == kCFUserNotificationDefaultResponse) {
589 // We need to let things unwind to main for the new state to get saved
590 doOnceInMain(^{
591 NSURL *url = [NSURL URLWithString: _isAccountICDP ? rejoinICDPUrl : castleKeychainUrl];
592 BOOL ok = [[LSApplicationWorkspace defaultWorkspace] openSensitiveURL:url withOptions:nil];
593 secnotice("cjr","kickOutChoice account is iCDP: %d", _isAccountICDP);
594 secnotice("cjr", "ok=%d opening %@", ok, url);
595 });
596 }
597 //alternate response: later -> call CD
598 else if (responseFlags == kCFUserNotificationAlternateResponse) {
599 // We need to let things unwind to main for the new state to get saved
600 doOnceInMain(^{
601 if(_isAccountICDP){
602 CDPFollowUpController *cdpd = [[CDPFollowUpController alloc] init];
603 NSError *localError = nil;
604 CDPFollowUpContext *context = [CDPFollowUpContext contextForStateRepair];
605 [cdpd postFollowUpWithContext:context error:&localError ];
606 if(localError){
607 secnotice("cjr", "request to CoreCDP to follow up failed: %@", localError);
608 }
609 else{
610 secnotice("cjr", "CoreCDP handling follow up");
611 _hasPostedFollowupAndStillInError = true;
612 }
613 }
614 });
615
616 }
617 cancelCurrentAlert(true);
618 }
619
620
621 CFStringRef const CJRAggdDepartureReasonKey = CFSTR("com.apple.security.circlejoinrequested.departurereason");
622 CFStringRef const CJRAggdNumCircleDevicesKey = CFSTR("com.apple.security.circlejoinrequested.numcircledevices");
623
624 static void postKickedOutAlert(enum DepartureReason reason)
625 {
626 NSString *header = nil;
627 NSString *message = nil;
628
629 // <rdar://problem/20358568> iCDP telemetry: ☂ Statistics on circle reset and drop out events
630 ADClientSetValueForScalarKey(CJRAggdDepartureReasonKey, reason);
631
632 int64_t num_peers = 0;
633 CFArrayRef peerList = SOSCCCopyPeerPeerInfo(NULL);
634 if (peerList) {
635 num_peers = CFArrayGetCount(peerList);
636 if (num_peers > 99) {
637 // Round down # peers to 2 significant digits
638 int factor;
639 for (factor = 10; num_peers >= 100*factor; factor *= 10) ;
640 num_peers = (num_peers / factor) * factor;
641 }
642 CFRelease(peerList);
643 }
644 ADClientSetValueForScalarKey(CJRAggdNumCircleDevicesKey, num_peers);
645
646 debugState = @"pKOA A";
647 secnotice("cjr", "DepartureReason %d", reason);
648 switch (reason) {
649 case kSOSDiscoveredRetirement:
650 case kSOSLostPrivateKey:
651 case kSOSWithdrewMembership:
652 // Was: SEC_CK_CR_BODY_WITHDREW
653 // "... if you turn off a switch you have some idea why the light is off" - Murf
654 return;
655
656 case kSOSNeverAppliedToCircle:
657 // We didn't get kicked out, we were never here. This should only happen if we changed iCloud accounts
658 // (and we had sync on in the previous one, and never had it on in the new one). As this is explicit
659 // user action alot of the "Light switch" argument (above) applies.
660 return;
661
662 case kSOSPasswordChanged:
663 case kSOSNeverLeftCircle:
664 case kSOSMembershipRevoked:
665 case kSOSLeftUntrustedCircle:
666 default:
667 header = (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_PWD_REQUIRED_TITLE);
668 message = (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_PWD_REQUIRED_BODY_IOS);
669 break;
670 }
671
672 if (CPIsInternalDevice()) {
673 static const char *departureReasonStrings[] = {
674 "kSOSDepartureReasonError",
675 "kSOSNeverLeftCircle",
676 "kSOSWithdrewMembership",
677 "kSOSMembershipRevoked",
678 "kSOSLeftUntrustedCircle",
679 "kSOSNeverAppliedToCircle",
680 "kSOSDiscoveredRetirement",
681 "kSOSLostPrivateKey",
682 "kSOSPasswordChanged",
683 "unknown reason"
684 };
685 int idx = (kSOSDepartureReasonError <= reason && reason <= kSOSLostPrivateKey) ? reason : (kSOSLostPrivateKey + 1);
686 #pragma clang diagnostic push
687 #pragma clang diagnostic ignored "-Wformat-nonliteral"
688 NSString *reason_str = [NSString stringWithFormat:(__bridge_transfer NSString *) SecCopyCKString(SEC_CK_CR_REASON_INTERNAL),
689 departureReasonStrings[idx]];
690 #pragma clang diagnostic pop
691 message = [message stringByAppendingString: reason_str];
692 }
693
694 NSDictionary *kickedAttributes = @{
695 (id) kCFUserNotificationAlertHeaderKey : header,
696 (id) kCFUserNotificationAlertMessageKey : message,
697 (id) kCFUserNotificationDefaultButtonTitleKey : (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_CONTINUE),
698 (id) kCFUserNotificationAlternateButtonTitleKey: (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_NOT_NOW),
699 (id) kCFUserNotificationAlertTopMostKey : @YES,
700 (__bridge id) SBUserNotificationDismissOnLock : @NO,
701 (__bridge id) SBUserNotificationDontDismissOnUnlock : @YES,
702 };
703 SInt32 err = 0;
704
705 if (currentAlertIsForKickOut) {
706 debugState = @"pKOA B";
707 secnotice("cjr", "Updating existing alert %@ with %@", currentAlert, kickedAttributes);
708 CFUserNotificationUpdate(currentAlert, 0, kCFUserNotificationPlainAlertLevel, (__bridge CFDictionaryRef) kickedAttributes);
709 } else {
710 debugState = @"pKOA C";
711
712 CFUserNotificationRef note = CFUserNotificationCreate(NULL, 0.0, kCFUserNotificationPlainAlertLevel, &err, (__bridge CFDictionaryRef) kickedAttributes);
713 assert((note == NULL) == (err != 0));
714 if (err) {
715 secnotice("cjr", "Can't make kicked out notification err=%x", (int)err);
716 CFReleaseNull(note);
717 } else {
718 currentAlertIsForApplicants = false;
719 currentAlertIsForKickOut = true;
720
721 currentAlert = note;
722 secnotice("cjr", "New ko alert %@ a=%@", currentAlert, kickedAttributes);
723 currentAlertSource = CFUserNotificationCreateRunLoopSource(NULL, currentAlert, kickOutChoice, 0);
724 CFRunLoopAddSource(CFRunLoopGetCurrent(), currentAlertSource, kCFRunLoopDefaultMode);
725 int backupStateChangeToken;
726 notify_register_dispatch("com.apple.EscrowSecurityAlert.reset", &backupStateChangeToken, dispatch_get_main_queue(), ^(int token) {
727 if (currentAlert == note) {
728 secnotice("cjr", "Backup state might have changed (dS=%@)", debugState);
729 postKickedOutAlert(reason);
730 } else {
731 secnotice("cjr", "Backup state may have changed, but we don't care anymore (dS=%@)", debugState);
732 }
733 });
734
735 debugState = @"pKOA D";
736 CFRunLoopRun();
737 debugState = @"pKOA E";
738 notify_cancel(backupStateChangeToken);
739 }
740 }
741 debugState = @"pKOA Z";
742 }
743
744
745 static bool processEvents()
746 {
747 debugState = @"processEvents A";
748
749 CFErrorRef error = NULL;
750 CFErrorRef departError = NULL;
751 SOSCCStatus circleStatus = SOSCCThisDeviceIsInCircle(&error);
752 NSDate *nowish = [NSDate date];
753 PersistentState *state = [PersistentState loadFromStorage];
754 enum DepartureReason departureReason = SOSCCGetLastDepartureReason(&departError);
755 secnotice("cjr", "CircleStatus %d -> %d{%d} (s=%p)", state.lastCircleStatus, circleStatus, departureReason, state);
756
757 // Pending application reminder
758 NSTimeInterval timeUntilApplicationAlert = [state.pendingApplicationReminder timeIntervalSinceDate:nowish];
759 secnotice("cjr", "Time until pendingApplicationReminder (%@) %f", [state.pendingApplicationReminder debugDescription], timeUntilApplicationAlert);
760 if (circleStatus == kSOSCCRequestPending) {
761 if (timeUntilApplicationAlert <= 0) {
762 debugState = @"reminderAlert";
763 postApplicationReminderAlert(nowish, state, state.pendingApplicationReminderAlertInterval);
764 } else {
765 scheduleActivity(ceil(timeUntilApplicationAlert));
766 }
767 }
768
769 PSKeychainSyncIsUsingICDP();
770
771 if(_isAccountICDP){
772 if((circleStatus == kSOSCCError || circleStatus == kSOSCCCircleAbsent || circleStatus == kSOSCCNotInCircle) && _hasPostedFollowupAndStillInError == false) {
773 secnotice("cjr", "error from SOSCCThisDeviceIsInCircle: %@", error);
774 secnotice("cjr", "iCDP: We need to get back into the circle");
775 doOnceInMain(^{
776 if(_isAccountICDP){
777 NSError *localError = nil;
778 CDPFollowUpController *cdpd = [[CDPFollowUpController alloc] init];
779 CDPFollowUpContext *context = [CDPFollowUpContext contextForStateRepair];
780 [cdpd postFollowUpWithContext:context error:&localError ];
781 if(localError){
782 secnotice("cjr", "request to CoreCDP to follow up failed: %@", localError);
783 }
784 else{
785 secnotice("cjr", "CoreCDP handling follow up");
786 _hasPostedFollowupAndStillInError = true;
787 }
788 }
789 else{
790 postKickedOutAlert(kSOSPasswordChanged);
791 state.lastCircleStatus = kSOSCCError;
792 [state writeToStorage];
793 }
794 });
795 state.lastCircleStatus = circleStatus;
796 return false;
797 }
798 else if(circleStatus == kSOSCCInCircle){
799 secnotice("cjr", "follow up should be resolved");
800 _hasPostedFollowupAndStillInError = false;
801 }
802 else{
803 secnotice("cjr", "followup not resolved");
804 return false;
805 }
806 }
807 else if(circleStatus == kSOSCCError && state.lastCircleStatus != kSOSCCError && (departureReason == kSOSNeverLeftCircle)
808 && !_isAccountICDP) {
809 secnotice("cjr", "SA: error from SOSCCThisDeviceIsInCircle: %@", error);
810 CFIndex errorCode = CFErrorGetCode(error);
811 if(errorCode == kSOSErrorPublicKeyAbsent){
812 secnotice("cjr", "SA: We need the password to re-validate ourselves - it's changed on another device");
813 postKickedOutAlert(kSOSPasswordChanged);
814 state.lastCircleStatus = kSOSCCError;
815 [state writeToStorage];
816 return true;
817 }
818 }
819 // No longer in circle?
820 if ((state.lastCircleStatus == kSOSCCInCircle && (circleStatus == kSOSCCNotInCircle || circleStatus == kSOSCCCircleAbsent)) ||
821 (state.lastCircleStatus == kSOSCCCircleAbsent && circleStatus == kSOSCCNotInCircle && state.absentCircleWithNoReason) ||
822 state.debugShowLeftReason) {
823 // Used to be in the circle, now we aren't - tell the user why
824 debugState = @"processEvents B";
825
826 if (state.debugShowLeftReason) {
827 secnotice("cjr", "debugShowLeftReason: %@", state.debugShowLeftReason);
828 departureReason = [state.debugShowLeftReason intValue];
829 state.debugShowLeftReason = nil;
830 CFReleaseNull(departError);
831 [state writeToStorage];
832 }
833
834 if (departureReason != kSOSDepartureReasonError) {
835 state.absentCircleWithNoReason = (circleStatus == kSOSCCCircleAbsent && departureReason == kSOSNeverLeftCircle);
836 secnotice("cjr", "Depature reason %d", departureReason);
837 if(!_isAccountICDP){
838 secnotice("cjr", "posting revocation notification!");
839 postKickedOutAlert(departureReason);
840 }
841 else if(_isAccountICDP && _hasPostedFollowupAndStillInError == false){
842 NSError *localError = nil;
843 CDPFollowUpController *cdpd = [[CDPFollowUpController alloc] init];
844 CDPFollowUpContext *context = [CDPFollowUpContext contextForStateRepair];
845 [cdpd postFollowUpWithContext:context error:&localError ];
846 if(localError){
847 secnotice("cjr", "request to CoreCDP to follow up failed: %@", localError);
848 }
849 else{
850 secnotice("cjr", "CoreCDP handling follow up");
851 _hasPostedFollowupAndStillInError = true;
852 }
853 }
854 else{
855 secnotice("cjr", "still waiting for followup to resolve");
856 }
857 secnotice("cjr", "pKOA returned (cS %d lCS %d)", circleStatus, state.lastCircleStatus);
858 } else {
859 secnotice("cjr", "Couldn't get last departure reason: %@", departError);
860 }
861
862 }
863
864 // Circle applications: pending request(s) started / completed
865 debugState = @"processEvents C";
866 if (circleStatus != state.lastCircleStatus) {
867 SOSCCStatus lastCircleStatus = state.lastCircleStatus;
868 state.lastCircleStatus = circleStatus;
869
870 if (lastCircleStatus != kSOSCCRequestPending && circleStatus == kSOSCCRequestPending) {
871 secnotice("cjr", "Pending request started");
872 state.applicationDate = nowish;
873 state.pendingApplicationReminder = [nowish dateByAddingTimeInterval: state.pendingApplicationReminderAlertInterval];
874 scheduleActivity(state.pendingApplicationReminderAlertInterval);
875 }
876 if (lastCircleStatus == kSOSCCRequestPending && circleStatus != kSOSCCRequestPending) {
877 secnotice("cjr", "Pending request completed");
878 state.applicationDate = [NSDate distantPast];
879 state.pendingApplicationReminder = [NSDate distantFuture];
880 }
881
882 [state writeToStorage];
883 }
884
885 if (circleStatus != kSOSCCInCircle) {
886 if (circleStatus == kSOSCCRequestPending && currentAlert) {
887 int notifyToken = 0;
888 CFUserNotificationRef postedAlert = currentAlert;
889
890 debugState = @"processEvents D1";
891 notify_register_dispatch(kSOSCCCircleChangedNotification, &notifyToken, dispatch_get_main_queue(), ^(int token) {
892 if (postedAlert != currentAlert) {
893 secnotice("cjr", "-- CC after original alert gone (currentAlertIsForApplicants %d, pA %p, cA %p -- %@)",
894 currentAlertIsForApplicants, postedAlert, currentAlert, currentAlert);
895 notify_cancel(token);
896 } else {
897 CFErrorRef localError = NULL;
898 SOSCCStatus newCircleStatus = SOSCCThisDeviceIsInCircle(&localError);
899 if (newCircleStatus != kSOSCCRequestPending) {
900 if (newCircleStatus == kSOSCCError)
901 secnotice("cjr", "No longer pending (nCS=%d, alert=%@) error: %@", newCircleStatus, currentAlert, localError);
902 else
903 secnotice("cjr", "No longer pending (nCS=%d, alert=%@)", newCircleStatus, currentAlert);
904 cancelCurrentAlert(true);
905 } else {
906 secnotice("cjr", "Still pending...");
907 }
908 CFReleaseNull(localError);
909 }
910 });
911 debugState = @"processEvents D2";
912 secnotice("cjr", "NOTE: currentAlertIsForApplicants %d, token %d", currentAlertIsForApplicants, notifyToken);
913 CFRunLoopRun();
914 return true;
915 }
916 debugState = @"processEvents D4";
917 secnotice("cjr", "SOSCCThisDeviceIsInCircle status %d, not checking applicants", circleStatus);
918 return false;
919 }
920
921
922 // Applicants
923 debugState = @"processEvents E";
924 applicants = [NSMutableDictionary new];
925 for (id applicantInfo in (__bridge_transfer NSArray *) SOSCCCopyApplicantPeerInfo(&error)) {
926 Applicant *applicant = [[Applicant alloc] initWithPeerInfo:(__bridge SOSPeerInfoRef) applicantInfo];
927 applicants[applicant.idString] = applicant;
928 }
929
930 // Log error from SOSCCCopyApplicantPeerInfo() above?
931 CFReleaseNull(error);
932
933 int notify_token = -42;
934 debugState = @"processEvents F";
935 int notify_register_status = notify_register_dispatch(kSOSCCCircleChangedNotification, &notify_token, dispatch_get_main_queue(), ^(int token) {
936 secnotice("cjr", "Notified: %s", kSOSCCCircleChangedNotification);
937 CFErrorRef circleStatusError = NULL;
938
939 bool needsUpdate = false;
940 CFErrorRef copyPeerError = NULL;
941 NSMutableSet *newIds = [NSMutableSet new];
942 for (id applicantInfo in (__bridge_transfer NSArray *) SOSCCCopyApplicantPeerInfo(&copyPeerError)) {
943 Applicant *newApplicant = [[Applicant alloc] initWithPeerInfo:(__bridge SOSPeerInfoRef) applicantInfo];
944 [newIds addObject:newApplicant.idString];
945 Applicant *existingApplicant = applicants[newApplicant.idString];
946 if (existingApplicant) {
947 switch (existingApplicant.applicantUIState) {
948 case ApplicantWaiting:
949 applicants[newApplicant.idString] = newApplicant;
950 break;
951
952 case ApplicantOnScreen:
953 newApplicant.applicantUIState = ApplicantOnScreen;
954 applicants[newApplicant.idString] = newApplicant;
955 break;
956
957 default:
958 secnotice("cjr", "Update to %@ >> %@ with pending order, should work out ok though", existingApplicant, newApplicant);
959 break;
960 }
961 } else {
962 needsUpdate = true;
963 applicants[newApplicant.idString] = newApplicant;
964 }
965 }
966 if (copyPeerError) {
967 secnotice("cjr", "Could not update peer info array: %@", copyPeerError);
968 CFRelease(copyPeerError);
969 return;
970 }
971
972 NSMutableArray *idsToRemoveFromApplicants = [NSMutableArray new];
973 for (NSString *exisitngId in [applicants keyEnumerator]) {
974 if (![newIds containsObject:exisitngId]) {
975 [idsToRemoveFromApplicants addObject:exisitngId];
976 needsUpdate = true;
977 }
978 }
979 [applicants removeObjectsForKeys:idsToRemoveFromApplicants];
980
981 if (newIds.count == 0) {
982 secnotice("cjr", "All applicants were handled elsewhere");
983 cancelCurrentAlert(true);
984 }
985 SOSCCStatus currentCircleStatus = SOSCCThisDeviceIsInCircle(&circleStatusError);
986 if (kSOSCCInCircle != currentCircleStatus) {
987 secnotice("cjr", "Left circle (%d), not handling remaining %lu applicants", currentCircleStatus, (unsigned long)newIds.count);
988 cancelCurrentAlert(true);
989 }
990 if (needsUpdate) {
991 askAboutAll(false);
992 } else {
993 secnotice("cjr", "needsUpdate false, not updating alert");
994 }
995 // Log circleStatusError?
996 CFReleaseNull(circleStatusError);
997 });
998 secnotice("cjr", "ACC token %d, status %d", notify_token, notify_register_status);
999 debugState = @"processEvents F2";
1000
1001 if (applicants.count == 0) {
1002 secnotice("cjr", "No applicants");
1003 } else {
1004 debugState = @"processEvents F3";
1005 askAboutAll(false);
1006 debugState = @"processEvents F4";
1007 if (currentAlert) {
1008 debugState = @"processEvents F5";
1009 CFRunLoopRun();
1010 }
1011 }
1012
1013 debugState = @"processEvents F6";
1014 notify_cancel(notify_token);
1015 debugState = @"processEvents DONE";
1016
1017 return false;
1018 }
1019
1020
1021 int main (int argc, const char * argv[]) {
1022
1023 xpc_transaction_begin();
1024
1025 @autoreleasepool {
1026
1027 // NOTE: DISPATCH_QUEUE_PRIORITY_LOW will not actually manage to drain events in a lot of cases (like circleStatus != kSOSCCInCircle)
1028 xpc_set_event_stream_handler("com.apple.notifyd.matching", dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^(xpc_object_t object) {
1029 char *event_description = xpc_copy_description(object);
1030 const char *notificationName = xpc_dictionary_get_string(object, "Notification");
1031
1032 if (notificationName && strcmp(notificationName, kUserKeybagStateChangeNotification)==0) {
1033 secnotice("cjr", "keybag changed!");
1034 keybagStateChange();
1035 }
1036
1037 secnotice("cjr", "notifyd event: %s\nAlert (%p) %s %s\ndebugState: %@", event_description, currentAlert,
1038 currentAlertIsForApplicants ? "for applicants" : "!applicants",
1039 currentAlertIsForKickOut ? "KO" : "!KO", debugState);
1040 free(event_description);
1041 });
1042
1043 xpc_activity_register(kLaunchLaterXPCName, XPC_ACTIVITY_CHECK_IN, ^(xpc_activity_t activity) {
1044 });
1045
1046 int falseInARow = 0;
1047 while (falseInARow < 2) {
1048 if (processEvents()) {
1049 secnotice("cjr", "Processed events!!!");
1050 falseInARow = 0;
1051 } else {
1052 falseInARow++;
1053 }
1054 cancelCurrentAlert(false);
1055 if (doOnceInMainBlockChain) {
1056 doOnceInMainBlockChain();
1057 doOnceInMainBlockChain = NULL;
1058 }
1059 }
1060 }
1061
1062 secnotice("cjr", "Done");
1063 xpc_transaction_end();
1064 return(0);
1065 }