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 }