/ keychain / securityd / PolicyReporter.m
PolicyReporter.m
  1  #include "PolicyReporter.h"
  2  
  3  #import <CloudKit/CKContainer_Private.h>
  4  #import <CloudKit/CloudKit.h>
  5  #import <CoreFoundation/CFPriv.h>
  6  #import <Foundation/Foundation.h>
  7  #import <Foundation/NSURLSession.h>
  8  #import <dispatch/dispatch.h>
  9  #import <os/feature_private.h>
 10  #import <os/variant_private.h>
 11  
 12  #include <notify.h>
 13  #include <stdio.h>
 14  #include <stdlib.h>
 15  #include <string.h>
 16  #include <unistd.h>
 17  #include <xpc/private.h>
 18  #include <Security/SecItem.h>
 19  #include <Security/SecItemPriv.h>
 20  #include <Security/Security.h>
 21  #include <keychain/SecureObjectSync/SOSCloudCircle.h>
 22  #include <keychain/SecureObjectSync/SOSViews.h>
 23  #include <utilities/SecAKSWrappers.h>
 24  #import "utilities/debugging.h"
 25  #import "TrustedPeers/TrustedPeers.h"
 26  
 27  // Stolen from keychain/SecureObjectSync/SOSEngine.c
 28  
 29  static NSString* getSOSView(id object, NSString* itemClass) {
 30      if (![object isKindOfClass:[NSDictionary class]]) {
 31          return nil;
 32      }
 33  
 34      NSString *viewHint = object[(NSString*)kSecAttrSyncViewHint];
 35      if (viewHint != nil) {
 36          return viewHint;
 37      } else {
 38          NSString *ag = object[(NSString*)kSecAttrAccessGroup];
 39          if ([itemClass isEqualToString: (NSString*)kSecClassKey] && [ag isEqualToString: @"com.apple.security.sos"]) {
 40              return (NSString*)kSOSViewiCloudIdentity;
 41          } else if ([ag isEqualToString: @"com.apple.cfnetwork"]) {
 42              return (NSString*)kSOSViewAutofillPasswords;
 43          } else if ([ag isEqualToString: @"com.apple.safari.credit-cards"]) {
 44              return (NSString*)kSOSViewSafariCreditCards;
 45          } else if ([itemClass isEqualToString: (NSString*)kSecClassGenericPassword]) {
 46              if ([ag isEqualToString: @"apple"] &&
 47                  [object[(NSString*)kSecAttrService] isEqualToString: @"AirPort"]) {
 48                  return (NSString*)kSOSViewWiFi;
 49              } else if ([ag isEqualToString: @"com.apple.sbd"]) {
 50                  return (NSString*)kSOSViewBackupBagV0;
 51              } else {
 52                  return (NSString*)kSOSViewOtherSyncable; // (genp)
 53              }
 54          } else {
 55              return (NSString*)kSOSViewOtherSyncable; // (inet || keys)
 56          }
 57      }
 58  }
 59  
 60  static inline NSNumber *now_msecs() {
 61      return @(((long)[[NSDate date] timeIntervalSince1970] * 1000));
 62  }
 63  
 64  static NSString* cloudKitDeviceID() {
 65    __block NSString* ret = nil;
 66  
 67    if (!os_variant_has_internal_diagnostics("com.apple.security")) {
 68      return nil;
 69    }
 70    CKContainer *container = [CKContainer containerWithIdentifier:@"com.apple.security.keychain"];
 71    dispatch_semaphore_t sem = dispatch_semaphore_create(0);
 72    [container fetchCurrentDeviceIDWithCompletionHandler:^(NSString* deviceID, NSError* error) {
 73        if (error != nil) {
 74  	NSLog(@"failed to fetch CK deviceID: %@", error);
 75        } else {
 76  	ret = deviceID;
 77        }
 78        dispatch_semaphore_signal(sem);
 79      }];
 80    dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
 81    return ret;
 82  }
 83  
 84  static void reportStats(unsigned expected_mismatches, unsigned real_mismatches, NSArray<NSDictionary*>* reportedMismatches) {
 85      NSURLSessionConfiguration *defaultConfiguration = [NSURLSessionConfiguration ephemeralSessionConfiguration];
 86      NSURLSession *session = [NSURLSession sessionWithConfiguration:defaultConfiguration];
 87      NSURL *endpoint = [NSURL URLWithString:@"https://xp.apple.com/report/2/xp_sear_keysync"];
 88      NSMutableURLRequest *req = [[NSMutableURLRequest alloc] init];
 89      req.URL = endpoint;
 90      req.HTTPMethod = @"POST";
 91      NSMutableDictionary *dict = [@{
 92          @"expectedMismatches": @(expected_mismatches),
 93          @"realMismatches": @(real_mismatches),
 94          @"eventTime": now_msecs(),
 95          @"topic": @"xp_sear_keysync",
 96          @"eventType": @"policy-dryrun",
 97      } mutableCopy];
 98      NSDictionary *version = CFBridgingRelease(_CFCopySystemVersionDictionary());
 99      NSString *build = version[(__bridge NSString *)_kCFSystemVersionBuildVersionKey];
100      if (build != nil) {
101        dict[@"build"] = build;
102      } else {
103        NSLog(@"Unable to find out build version");
104      }
105      NSString *product = version[(__bridge NSString *)_kCFSystemVersionProductNameKey];
106      if (product != nil) {
107        dict[@"product"] = product;
108      } else {
109        NSLog(@"Unable to find out build product");
110      }
111      NSString* ckDeviceID = cloudKitDeviceID();
112      if (ckDeviceID) {
113        dict[@"SFAnalyticsDeviceID"] = ckDeviceID;
114        dict[@"ckdeviceID"] = ckDeviceID;
115      } else {
116        NSLog(@"Unable to fetch CK device ID");
117      }
118  
119      dict[@"mismatches"] = reportedMismatches;
120  
121      NSArray<NSDictionary*>* events = @[dict];
122      NSDictionary *wrapper = @{
123  			      @"postTime": @([[NSDate date] timeIntervalSince1970] * 1000),
124  			      @"events": events,
125      };
126      NSError *encodeError = nil;
127      NSData* post_data = [NSJSONSerialization dataWithJSONObject:wrapper options:0 error:&encodeError];
128      if (post_data == nil || encodeError != nil) {
129          NSLog(@"failed to encode data: %@", encodeError);
130          return;
131      }
132      NSLog(@"logging %@, %@", wrapper, post_data);
133      req.HTTPBody = post_data;
134      dispatch_semaphore_t sem = dispatch_semaphore_create(0);
135      NSURLSessionDataTask *task = [session dataTaskWithRequest:req completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
136              if (error != nil) {
137                  NSLog(@"splunk upload failed: %@", error);
138                  return;
139              }
140              NSHTTPURLResponse *httpResp = (NSHTTPURLResponse*)response;
141              if (httpResp.statusCode != 200) {
142                  NSLog(@"HTTP Post error: %@", httpResp);
143              }
144  	    NSLog(@"%@", httpResp);
145  	    if (data != nil) {
146  	      size_t length = [data length];
147  	      char *buf = malloc(length);
148  	      [data getBytes:buf length:length];
149  	      NSLog(@"%.*s", (int)length, buf);
150  	      free(buf);
151  	    }
152              dispatch_semaphore_signal(sem);
153          }];
154      [task resume];
155      dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
156  }
157  
158  static void oneReport(void) {
159      NSError* error = nil;
160  
161      // From Swift.policy, policy v5
162      TPPolicyDocument *tpd = [TPPolicyDocument policyDocWithHash:@"SHA256:O/ECQlWhvNlLmlDNh2+nal/yekUC87bXpV3k+6kznSo="
163                                                data:[[NSData alloc] initWithBase64EncodedString:
164                                                                         @"CAUSDgoGaVBob25lEgRmdWxsEgwKBGlQYWQSBGZ1bGwSDAoEaVBvZBIEZnVsbBILCgNNYWMSBGZ1bGwSDAoEaU1hYxIEZnVsbBINCgdBcHBsZVRWEgJ0dhIOCgVXYXRjaBIFd2F0Y2gSFwoOQXVkaW9BY2Nlc3NvcnkSBWF1ZGlvGhsKDEFwcGxpY2F0aW9ucxIEZnVsbBIFd2F0Y2gaHwoQU2VjdXJlT2JqZWN0U3luYxIEZnVsbBIFd2F0Y2gaHAoNRGV2aWNlUGFpcmluZxIEZnVsbBIFd2F0Y2gaGgoLQ3JlZGl0Q2FyZHMSBGZ1bGwSBXdhdGNoGhUKBkhlYWx0aBIEZnVsbBIFd2F0Y2gaLQoTTGltaXRlZFBlZXJzQWxsb3dlZBIEZnVsbBIFd2F0Y2gSAnR2EgVhdWRpbxokChVQcm90ZWN0ZWRDbG91ZFN0b3JhZ2USBGZ1bGwSBXdhdGNoGhcKCEFwcGxlUGF5EgRmdWxsEgV3YXRjaBoZCgpBdXRvVW5sb2NrEgRmdWxsEgV3YXRjaBoWCgdNYW5hdGVlEgRmdWxsEgV3YXRjaBoYCglQYXNzd29yZHMSBGZ1bGwSBXdhdGNoGhUKBkVuZ3JhbRIEZnVsbBIFd2F0Y2gaHgoEV2lGaRIEZnVsbBIFd2F0Y2gSAnR2EgVhdWRpbxoTCgRIb21lEgRmdWxsEgV3YXRjaCIbCgVhdWRpbxIEZnVsbBIFd2F0Y2gSBWF1ZGlvIhMKBGZ1bGwSBGZ1bGwSBXdhdGNoIhUKAnR2EgRmdWxsEgV3YXRjaBICdHYiFAoFd2F0Y2gSBGZ1bGwSBXdhdGNoMiIKFgAEIhICBHZ3aHQKCl5BcHBsZVBheSQSCEFwcGxlUGF5MiYKGAAEIhQCBHZ3aHQKDF5BdXRvVW5sb2NrJBIKQXV0b1VubG9jazIeChQABCIQAgR2d2h0CgheRW5ncmFtJBIGRW5ncmFtMh4KFAAEIhACBHZ3aHQKCF5IZWFsdGgkEgZIZWFsdGgyGgoSAAQiDgIEdndodAoGXkhvbWUkEgRIb21lMiAKFQAEIhECBHZ3aHQKCV5NYW5hdGVlJBIHTWFuYXRlZTI4CiEABCIdAgR2d2h0ChVeTGltaXRlZFBlZXJzQWxsb3dlZCQSE0xpbWl0ZWRQZWVyc0FsbG93ZWQyXQpQAAISHgAEIhoCBHZ3aHQKEl5Db250aW51aXR5VW5sb2NrJBIVAAQiEQIEdndodAoJXkhvbWVLaXQkEhUABCIRAgR2d2h0CgleQXBwbGVUViQSCU5vdFN5bmNlZDIrChsABCIXAgRhZ3JwCg9eWzAtOUEtWl17MTB9XC4SDEFwcGxpY2F0aW9uczLFAQqwAQACEjQAAQoTAAQiDwIFY2xhc3MKBl5nZW5wJAobAAQiFwIEYWdycAoPXmNvbS5hcHBsZS5zYmQkEj0AAQoTAAQiDwIFY2xhc3MKBl5rZXlzJAokAAQiIAIEYWdycAoYXmNvbS5hcHBsZS5zZWN1cml0eS5zb3MkEhkABCIVAgR2d2h0Cg1eQmFja3VwQmFnVjAkEhwABCIYAgR2d2h0ChBeaUNsb3VkSWRlbnRpdHkkEhBTZWN1cmVPYmplY3RTeW5jMmMKWwACEhIABCIOAgR2d2h0CgZeV2lGaSQSQwABChMABCIPAgVjbGFzcwoGXmdlbnAkChMABCIPAgRhZ3JwCgdeYXBwbGUkChUABCIRAgRzdmNlCgleQWlyUG9ydCQSBFdpRmkynQMKgwMAAhIYAAQiFAIEdndodAoMXlBDUy1CYWNrdXAkEhoABCIWAgR2d2h0Cg5eUENTLUNsb3VkS2l0JBIYAAQiFAIEdndodAoMXlBDUy1Fc2Nyb3ckEhUABCIRAgR2d2h0CgleUENTLUZERSQSGgAEIhYCBHZ3aHQKDl5QQ1MtRmVsZHNwYXIkEhoABCIWAgR2d2h0Cg5eUENTLU1haWxEcm9wJBIaAAQiFgIEdndodAoOXlBDUy1NYWlsZHJvcCQSGwAEIhcCBHZ3aHQKD15QQ1MtTWFzdGVyS2V5JBIXAAQiEwIEdndodAoLXlBDUy1Ob3RlcyQSGAAEIhQCBHZ3aHQKDF5QQ1MtUGhvdG9zJBIZAAQiFQIEdndodAoNXlBDUy1TaGFyaW5nJBIeAAQiGgIEdndodAoSXlBDUy1pQ2xvdWRCYWNrdXAkEh0ABCIZAgR2d2h0ChFeUENTLWlDbG91ZERyaXZlJBIaAAQiFgIEdndodAoOXlBDUy1pTWVzc2FnZSQSFVByb3RlY3RlZENsb3VkU3RvcmFnZTI6CisABCInAgRhZ3JwCh9eY29tLmFwcGxlLnNhZmFyaS5jcmVkaXQtY2FyZHMkEgtDcmVkaXRDYXJkczIuCiEABCIdAgRhZ3JwChVeY29tLmFwcGxlLmNmbmV0d29yayQSCVBhc3N3b3JkczJtClwAAhIeAAQiGgIEdndodAoSXkFjY2Vzc29yeVBhaXJpbmckEhoABCIWAgR2d2h0Cg5eTmFub1JlZ2lzdHJ5JBIcAAQiGAIEdndodAoQXldhdGNoTWlncmF0aW9uJBINRGV2aWNlUGFpcmluZzIOCgIABhIIQmFja3N0b3A=" options:0]];
165      
166      TPPolicy *policy = [tpd policyWithSecrets:@{} decrypter:nil error:&error];
167      if (error != nil) {
168          NSLog(@"policy error: %@", error);
169          return;
170      }
171      if (policy == nil) {
172          NSLog(@"policy is nil");
173          return;
174      }
175      TPSyncingPolicy* syncingPolicy = [policy syncingPolicyForModel:@"iPhone"
176                                           syncUserControllableViews:TPPBPeerStableInfo_UserControllableViewStatus_UNKNOWN
177                                                               error:&error];
178      if(syncingPolicy == nil || error != nil) {
179          NSLog(@"syncing policy is nil: %@", error);
180          return;
181      }
182  
183      unsigned real_mismatches = 0;
184      unsigned expected_mismatches = 0;
185      NSMutableArray<NSDictionary*>* reportedMismatches = [[NSMutableArray<NSDictionary*> alloc] init];
186  
187      NSArray<NSString*>* keychainClasses = @[(id)kSecClassInternetPassword,
188                                              (id)kSecClassGenericPassword,
189                                              (id)kSecClassKey,
190                                              (id)kSecClassCertificate];
191  
192      for(NSString* itemClass in keychainClasses) {
193          NSDictionary *query = @{ (id)kSecMatchLimit : (id)kSecMatchLimitAll,
194                                   (id)kSecClass : (id)itemClass,
195                                   (id)kSecReturnAttributes : @YES,
196                                   (id)kSecAttrSynchronizable: @YES,
197                                   (id)kSecUseDataProtectionKeychain: @YES,
198                                   (id)kSecUseAuthenticationUI : (id)kSecUseAuthenticationUISkip,
199                                   };
200  
201          NSArray *result;
202  
203          OSStatus status = SecItemCopyMatching((CFDictionaryRef)query, (void*)&result);
204          if (status) {
205              if (status == errSecItemNotFound) {
206                  NSLog(@"no items found matching: %@", query);
207                  continue;
208              } else {
209                  NSLog(@"SecItemCopyMatching(%@) failed: %d", query, (int)status);
210                  return;
211              }
212          }
213  
214          if (![result isKindOfClass:[NSArray class]]) {
215              NSLog(@"expected NSArray result from SecItemCopyMatching");
216              return;
217          }
218  
219          for (id a in result) {
220              NSLog(@"%@", a);
221              NSString *oldView = getSOSView(a, itemClass);
222              if (oldView != nil) {
223                  NSLog(@"old: %@", oldView);
224              }
225  
226              NSMutableDictionary* mutA = [a mutableCopy];
227              mutA[(id)kSecClass] = (id)itemClass;
228  
229              NSString* newView = [syncingPolicy mapDictionaryToView:mutA];
230              if (newView != nil) {
231                  NSLog(@"new: %@", newView);
232              }
233              if(oldView == nil ^ newView == nil) {
234                  NSLog(@"real mismatch: old view (%@) != new view (%@)", oldView, newView);
235                  ++real_mismatches;
236                  [reportedMismatches addObject: a];
237  
238                  } else if (oldView && newView && ![oldView isEqualToString: newView]) {
239                      if ([oldView hasPrefix:@"PCS-"] && [newView isEqualToString: @"ProtectedCloudStorage"]) {
240                          NSLog(@"(expected PCS mismatch): old view (%@) != new view (%@)", oldView, newView);
241                          ++expected_mismatches;
242  
243                      } else if([oldView isEqualToString:@"OtherSyncable"] && [newView isEqualToString: @"Applications"]) {
244                          NSLog(@"(expected 3rd party mismatch): old view (%@) != new view (%@)", oldView, newView);
245                          ++expected_mismatches;
246  
247                      } else if([oldView isEqualToString:@"OtherSyncable"] && [newView isEqualToString: @"Backstop"]) {
248                          NSLog(@"(expected backstop mismatch): old view (%@) != new view (%@)", oldView, newView);
249                          ++expected_mismatches;
250  
251  		    } else if([newView isEqualToString:@"NotSynced"]) {
252  			NSLog(@"(expected NotSynced mismatch): old view (%@) != new view (%@)", oldView, newView);
253  			++expected_mismatches;
254  			
255  		    } else if(([oldView isEqualToString:@"BackupBagV0"] || [oldView isEqualToString:@"iCloudIdentity"]) && [newView isEqualToString:@"SecureObjectSync"]) {
256  			NSLog(@"(expected BackupBag - SecureObjectSync mismatch): old view (%@) != new view (%@)", oldView, newView);
257  			++expected_mismatches;
258  
259  		    } else if(([oldView isEqualToString:@"AccessoryPairing"]
260  			       || [oldView isEqualToString:@"NanoRegistry"]
261  			       || [oldView isEqualToString:@"WatchMigration"]) && [newView isEqualToString:@"DevicePairing"]) {
262  			NSLog(@"(expected DevicePairing mismatch): old view (%@) != new view (%@)", oldView, newView);
263  			++expected_mismatches;
264  
265                      } else {
266                          NSLog(@"real mismatch: old view (%@) != new view (%@)", oldView, newView);
267                          ++real_mismatches;
268                          [reportedMismatches addObject: a];
269                      }
270                  }
271          }
272      }
273  
274      NSLog(@"%d expected_mismatches", expected_mismatches);
275      NSLog(@"%d real_mismatches", real_mismatches);
276      reportStats(expected_mismatches, real_mismatches, reportedMismatches);
277  }
278  
279  static dispatch_queue_t queue;
280  
281  static void report(void);
282  
283  static void maybeReport(void) {
284      CFErrorRef error = NULL;
285      bool locked = true;
286      if (!SecAKSGetIsLocked(&locked, &error)) {
287          secerror("PolicyReporter: %@", error);
288          CFReleaseNull(error);
289          xpc_object_t options = xpc_dictionary_create(NULL, NULL, 0);
290          xpc_dictionary_set_uint64(options, XPC_ACTIVITY_DELAY, random() % 60);
291          xpc_dictionary_set_uint64(options, XPC_ACTIVITY_GRACE_PERIOD, 30);
292          xpc_dictionary_set_bool(options, XPC_ACTIVITY_REPEATING, false);
293          xpc_dictionary_set_bool(options, XPC_ACTIVITY_ALLOW_BATTERY, true);
294          xpc_dictionary_set_string(options, XPC_ACTIVITY_PRIORITY, XPC_ACTIVITY_PRIORITY_UTILITY);
295          xpc_dictionary_set_bool(options, XPC_ACTIVITY_REQUIRE_NETWORK_CONNECTIVITY, true);
296  #if TARGET_OS_IPHONE
297          xpc_dictionary_set_bool(options, XPC_ACTIVITY_REQUIRES_CLASS_A, true);
298  #endif
299          xpc_activity_register("com.apple.security.securityd.policy-reporting2",
300                                options, ^(xpc_activity_t activity) {
301                                    report();
302                                });
303          return;
304      }
305  
306      if (locked) {
307          int token = 0;
308          notify_register_dispatch(kUserKeybagStateChangeNotification, &token, queue, ^(int t) {
309                  report();
310              });
311          return;
312      }
313      oneReport();
314  }
315  
316  static void report() {
317      @autoreleasepool {
318          maybeReport();
319      }
320  }
321  
322  void InitPolicyReporter(void) {
323      if (!os_feature_enabled(Security, securitydReportPolicy)) {
324          secnotice("securityd-PolicyReporter", "not enabled by feature flag");
325          return;
326      }
327  
328      srandom(getpid() ^ (int)time(NULL));
329      queue = dispatch_queue_create("com.apple.security.securityd_reporting", DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL);
330      xpc_object_t options = xpc_dictionary_create(NULL, NULL, 0);
331      xpc_dictionary_set_uint64(options, XPC_ACTIVITY_DELAY, random() % 3600);
332      xpc_dictionary_set_uint64(options, XPC_ACTIVITY_GRACE_PERIOD, 1800);
333      xpc_dictionary_set_uint64(options, XPC_ACTIVITY_INTERVAL, 3600);
334      xpc_dictionary_set_bool(options, XPC_ACTIVITY_REPEATING, true);
335      xpc_dictionary_set_bool(options, XPC_ACTIVITY_ALLOW_BATTERY, true);
336      xpc_dictionary_set_string(options, XPC_ACTIVITY_PRIORITY, XPC_ACTIVITY_PRIORITY_UTILITY);
337      xpc_dictionary_set_bool(options, XPC_ACTIVITY_REQUIRE_NETWORK_CONNECTIVITY, true);
338  #if TARGET_OS_IPHONE
339      xpc_dictionary_set_bool(options, XPC_ACTIVITY_REQUIRES_CLASS_A, true);
340  #endif
341      
342      xpc_activity_register("com.apple.security.securityd.policy-reporting",
343                            options, ^(xpc_activity_t activity) {
344                                report();
345                            });
346  }