]> git.saurik.com Git - apple/security.git/blob - Analytics/SFAnalytics.m
Security-58286.220.15.tar.gz
[apple/security.git] / Analytics / SFAnalytics.m
1 /*
2 * Copyright (c) 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 #if __OBJC2__
25
26 #import "SFAnalytics+Internal.h"
27 #import "SFAnalyticsDefines.h"
28 #import "SFAnalyticsActivityTracker+Internal.h"
29 #import "SFAnalyticsSampler+Internal.h"
30 #import "SFAnalyticsMultiSampler+Internal.h"
31 #import "SFAnalyticsSQLiteStore.h"
32 #import "utilities/debugging.h"
33 #import <utilities/SecFileLocations.h>
34 #import <objc/runtime.h>
35 #import <sys/stat.h>
36 #import <CoreFoundation/CFPriv.h>
37
38 // SFAnalyticsDefines constants
39 NSString* const SFAnalyticsTableSuccessCount = @"success_count";
40 NSString* const SFAnalyticsTableHardFailures = @"hard_failures";
41 NSString* const SFAnalyticsTableSoftFailures = @"soft_failures";
42 NSString* const SFAnalyticsTableSamples = @"samples";
43 NSString* const SFAnalyticsTableAllEvents = @"all_events";
44
45 NSString* const SFAnalyticsColumnSuccessCount = @"success_count";
46 NSString* const SFAnalyticsColumnHardFailureCount = @"hard_failure_count";
47 NSString* const SFAnalyticsColumnSoftFailureCount = @"soft_failure_count";
48 NSString* const SFAnalyticsColumnSampleValue = @"value";
49 NSString* const SFAnalyticsColumnSampleName = @"name";
50
51 NSString* const SFAnalyticsEventTime = @"eventTime";
52 NSString* const SFAnalyticsEventType = @"eventType";
53 NSString* const SFAnalyticsEventClassKey = @"eventClass";
54
55 NSString* const SFAnalyticsAttributeErrorUnderlyingChain = @"errorChain";
56 NSString* const SFAnalyticsAttributeErrorDomain = @"errorDomain";
57 NSString* const SFAnalyticsAttributeErrorCode = @"errorCode";
58
59 NSString* const SFAnalyticsUserDefaultsSuite = @"com.apple.security.analytics";
60
61 char* const SFAnalyticsFireSamplersNotification = "com.apple.security.sfanalytics.samplers";
62
63 NSString* const SFAnalyticsTopicKeySync = @"KeySyncTopic";
64 NSString* const SFAnaltyicsTopicTrust = @"TrustTopic";
65
66 NSString* const SFAnalyticsTableSchema = @"CREATE TABLE IF NOT EXISTS hard_failures (\n"
67 @"id INTEGER PRIMARY KEY AUTOINCREMENT,\n"
68 @"timestamp REAL,"
69 @"data BLOB\n"
70 @");\n"
71 @"CREATE TRIGGER IF NOT EXISTS maintain_ring_buffer_hard_failures AFTER INSERT ON hard_failures\n"
72 @"BEGIN\n"
73 @"DELETE FROM hard_failures WHERE id != NEW.id AND id % 1000 = NEW.id % 1000;\n"
74 @"END;\n"
75 @"CREATE TABLE IF NOT EXISTS soft_failures (\n"
76 @"id INTEGER PRIMARY KEY AUTOINCREMENT,\n"
77 @"timestamp REAL,"
78 @"data BLOB\n"
79 @");\n"
80 @"CREATE TRIGGER IF NOT EXISTS maintain_ring_buffer_soft_failures AFTER INSERT ON soft_failures\n"
81 @"BEGIN\n"
82 @"DELETE FROM soft_failures WHERE id != NEW.id AND id % 1000 = NEW.id % 1000;\n"
83 @"END;\n"
84 @"CREATE TABLE IF NOT EXISTS all_events (\n"
85 @"id INTEGER PRIMARY KEY AUTOINCREMENT,\n"
86 @"timestamp REAL,"
87 @"data BLOB\n"
88 @");\n"
89 @"CREATE TRIGGER IF NOT EXISTS maintain_ring_buffer_all_events AFTER INSERT ON all_events\n"
90 @"BEGIN\n"
91 @"DELETE FROM all_events WHERE id != NEW.id AND id % 10000 = NEW.id % 10000;\n"
92 @"END;\n"
93 @"CREATE TABLE IF NOT EXISTS samples (\n"
94 @"id INTEGER PRIMARY KEY AUTOINCREMENT,\n"
95 @"timestamp REAL,\n"
96 @"name STRING,\n"
97 @"value REAL\n"
98 @");\n"
99 @"CREATE TRIGGER IF NOT EXISTS maintain_ring_buffer_samples AFTER INSERT ON samples\n"
100 @"BEGIN\n"
101 @"DELETE FROM samples WHERE id != NEW.id AND id % 1000 = NEW.id % 1000;\n"
102 @"END;\n"
103 @"CREATE TABLE IF NOT EXISTS success_count (\n"
104 @"event_type STRING PRIMARY KEY,\n"
105 @"success_count INTEGER,\n"
106 @"hard_failure_count INTEGER,\n"
107 @"soft_failure_count INTEGER\n"
108 @");\n";
109
110 NSUInteger const SFAnalyticsMaxEventsToReport = 1000;
111
112 NSString* const SFAnalyticsErrorDomain = @"com.apple.security.sfanalytics";
113
114 // Local constants
115 NSString* const SFAnalyticsEventBuild = @"build";
116 NSString* const SFAnalyticsEventProduct = @"product";
117 const NSTimeInterval SFAnalyticsSamplerIntervalOncePerReport = -1.0;
118
119 @interface SFAnalytics ()
120 @property (nonatomic) SFAnalyticsSQLiteStore* database;
121 @property (nonatomic) dispatch_queue_t queue;
122 @end
123
124 @implementation SFAnalytics {
125 SFAnalyticsSQLiteStore* _database;
126 dispatch_queue_t _queue;
127 NSMutableDictionary<NSString*, SFAnalyticsSampler*>* _samplers;
128 NSMutableDictionary<NSString*, SFAnalyticsMultiSampler*>* _multisamplers;
129 unsigned int _disableLogging:1;
130 }
131
132 + (instancetype)logger
133 {
134 #if TARGET_OS_SIMULATOR
135 return nil;
136 #else
137
138 if (self == [SFAnalytics class]) {
139 secerror("attempt to instatiate abstract class SFAnalytics");
140 return nil;
141 }
142
143 SFAnalytics* logger = nil;
144 @synchronized(self) {
145 logger = objc_getAssociatedObject(self, "SFAnalyticsInstance");
146 if (!logger) {
147 logger = [[self alloc] init];
148 objc_setAssociatedObject(self, "SFAnalyticsInstance", logger, OBJC_ASSOCIATION_RETAIN);
149 }
150 }
151
152 [logger database]; // For unit testing so there's always a database. DB shouldn't be nilled in production though
153 return logger;
154 #endif
155 }
156
157 + (NSString*)databasePath
158 {
159 return nil;
160 }
161
162 + (NSString *)defaultAnalyticsDatabasePath:(NSString *)basename
163 {
164 WithPathInKeychainDirectory(CFSTR("Analytics"), ^(const char *path) {
165 #if TARGET_OS_IPHONE
166 mode_t permissions = 0775;
167 #else
168 mode_t permissions = 0700;
169 #endif // TARGET_OS_IPHONE
170 int ret = mkpath_np(path, permissions);
171 if (!(ret == 0 || ret == EEXIST)) {
172 secerror("could not create path: %s (%s)", path, strerror(ret));
173 }
174 chmod(path, permissions);
175 });
176 NSString *path = [NSString stringWithFormat:@"Analytics/%@.db", basename];
177 return [(__bridge_transfer NSURL*)SecCopyURLForFileInKeychainDirectory((__bridge CFStringRef)path) path];
178 }
179
180 + (NSInteger)fuzzyDaysSinceDate:(NSDate*)date
181 {
182 // Sentinel: it didn't happen at all
183 if (!date) {
184 return -1;
185 }
186
187 // Sentinel: it happened but we don't know when because the date doesn't make sense
188 // Magic number represents January 1, 2017.
189 if ([date compare:[NSDate dateWithTimeIntervalSince1970:1483228800]] == NSOrderedAscending) {
190 return 1000;
191 }
192
193 NSInteger secondsPerDay = 60 * 60 * 24;
194
195 NSTimeInterval timeIntervalSinceDate = [[NSDate date] timeIntervalSinceDate:date];
196 if (timeIntervalSinceDate < secondsPerDay) {
197 return 0;
198 }
199 else if (timeIntervalSinceDate < (secondsPerDay * 7)) {
200 return 1;
201 }
202 else if (timeIntervalSinceDate < (secondsPerDay * 30)) {
203 return 7;
204 }
205 else if (timeIntervalSinceDate < (secondsPerDay * 365)) {
206 return 30;
207 }
208 else {
209 return 365;
210 }
211 }
212
213 // Instantiate lazily so unit tests can have clean databases each
214 - (SFAnalyticsSQLiteStore*)database
215 {
216 if (!_database) {
217 _database = [SFAnalyticsSQLiteStore storeWithPath:self.class.databasePath schema:SFAnalyticsTableSchema];
218 if (!_database) {
219 seccritical("Did not get a database! (Client %@)", NSStringFromClass([self class]));
220 }
221 }
222 return _database;
223 }
224
225 - (void)removeState
226 {
227 [_samplers removeAllObjects];
228 [_multisamplers removeAllObjects];
229
230 __weak __typeof(self) weakSelf = self;
231 dispatch_sync(_queue, ^{
232 __strong __typeof(self) strongSelf = weakSelf;
233 if (strongSelf) {
234 [strongSelf.database close];
235 strongSelf->_database = nil;
236 }
237 });
238 }
239
240 - (void)setDateProperty:(NSDate*)date forKey:(NSString*)key
241 {
242 __weak __typeof(self) weakSelf = self;
243 dispatch_sync(_queue, ^{
244 __strong __typeof(self) strongSelf = weakSelf;
245 if (strongSelf) {
246 [strongSelf.database setDateProperty:date forKey:key];
247 }
248 });
249 }
250
251 - (NSDate*)datePropertyForKey:(NSString*)key
252 {
253 __block NSDate* result = nil;
254 __weak __typeof(self) weakSelf = self;
255 dispatch_sync(_queue, ^{
256 __strong __typeof(self) strongSelf = weakSelf;
257 if (strongSelf) {
258 result = [strongSelf.database datePropertyForKey:key];
259 }
260 });
261 return result;
262 }
263
264 + (void)addOSVersionToEvent:(NSMutableDictionary*)eventDict {
265 static dispatch_once_t onceToken;
266 static NSString *build = NULL;
267 static NSString *product = NULL;
268 dispatch_once(&onceToken, ^{
269 NSDictionary *version = CFBridgingRelease(_CFCopySystemVersionDictionary());
270 if (version == NULL)
271 return;
272 build = version[(__bridge NSString *)_kCFSystemVersionBuildVersionKey];
273 product = version[(__bridge NSString *)_kCFSystemVersionProductNameKey];
274 });
275 if (build) {
276 eventDict[SFAnalyticsEventBuild] = build;
277 }
278 if (product) {
279 eventDict[SFAnalyticsEventProduct] = product;
280 }
281 }
282
283 - (instancetype)init
284 {
285 if (self = [super init]) {
286 _queue = dispatch_queue_create("SFAnalytics data access queue", DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL);
287 _samplers = [NSMutableDictionary<NSString*, SFAnalyticsSampler*> new];
288 _multisamplers = [NSMutableDictionary<NSString*, SFAnalyticsMultiSampler*> new];
289 [self database]; // for side effect of instantiating DB object. Used for testing.
290 }
291
292 return self;
293 }
294
295 // MARK: Event logging
296
297 - (void)logSuccessForEventNamed:(NSString*)eventName
298 {
299 [self logEventNamed:eventName class:SFAnalyticsEventClassSuccess attributes:nil];
300 }
301
302 - (void)logHardFailureForEventNamed:(NSString*)eventName withAttributes:(NSDictionary*)attributes
303 {
304 [self logEventNamed:eventName class:SFAnalyticsEventClassHardFailure attributes:attributes];
305 }
306
307 - (void)logSoftFailureForEventNamed:(NSString*)eventName withAttributes:(NSDictionary*)attributes
308 {
309 [self logEventNamed:eventName class:SFAnalyticsEventClassSoftFailure attributes:attributes];
310 }
311
312 - (void)logResultForEvent:(NSString*)eventName hardFailure:(bool)hardFailure result:(NSError*)eventResultError
313 {
314 [self logResultForEvent:eventName hardFailure:hardFailure result:eventResultError withAttributes:nil];
315 }
316
317 - (void)logResultForEvent:(NSString*)eventName hardFailure:(bool)hardFailure result:(NSError*)eventResultError withAttributes:(NSDictionary*)attributes
318 {
319 if(!eventResultError) {
320 [self logSuccessForEventNamed:eventName];
321 } else {
322 // Make an Attributes dictionary
323 NSMutableDictionary* eventAttributes = nil;
324 if (attributes) {
325 eventAttributes = [attributes mutableCopy];
326 } else {
327 eventAttributes = [NSMutableDictionary dictionary];
328 }
329
330 /* if we have underlying errors, capture the chain below the top-most error */
331 NSError *underlyingError = eventResultError.userInfo[NSUnderlyingErrorKey];
332 if ([underlyingError isKindOfClass:[NSError class]]) {
333 NSMutableString *chain = [NSMutableString string];
334 int count = 0;
335 do {
336 [chain appendFormat:@"%@-%ld:", underlyingError.domain, (long)underlyingError.code];
337 underlyingError = underlyingError.userInfo[NSUnderlyingErrorKey];
338 } while (count++ < 5 && [underlyingError isKindOfClass:[NSError class]]);
339
340 eventAttributes[SFAnalyticsAttributeErrorUnderlyingChain] = chain;
341 }
342
343 eventAttributes[SFAnalyticsAttributeErrorDomain] = eventResultError.domain;
344 eventAttributes[SFAnalyticsAttributeErrorCode] = @(eventResultError.code);
345
346 if(hardFailure) {
347 [self logHardFailureForEventNamed:eventName withAttributes:eventAttributes];
348 } else {
349 [self logSoftFailureForEventNamed:eventName withAttributes:eventAttributes];
350 }
351 }
352 }
353
354 - (void)noteEventNamed:(NSString*)eventName
355 {
356 [self logEventNamed:eventName class:SFAnalyticsEventClassNote attributes:nil];
357 }
358
359 - (void)logEventNamed:(NSString*)eventName class:(SFAnalyticsEventClass)class attributes:(NSDictionary*)attributes
360 {
361 if (!eventName) {
362 secerror("SFAnalytics: attempt to log an event with no name");
363 return;
364 }
365
366 __weak __typeof(self) weakSelf = self;
367 dispatch_sync(_queue, ^{
368 __strong __typeof(self) strongSelf = weakSelf;
369 if (!strongSelf || strongSelf->_disableLogging) {
370 return;
371 }
372
373 NSDictionary* eventDict = [self eventDictForEventName:eventName withAttributes:attributes eventClass:class];
374 [strongSelf.database addEventDict:eventDict toTable:SFAnalyticsTableAllEvents];
375
376 if (class == SFAnalyticsEventClassHardFailure) {
377 [strongSelf.database addEventDict:eventDict toTable:SFAnalyticsTableHardFailures];
378 [strongSelf.database incrementHardFailureCountForEventType:eventName];
379 }
380 else if (class == SFAnalyticsEventClassSoftFailure) {
381 [strongSelf.database addEventDict:eventDict toTable:SFAnalyticsTableSoftFailures];
382 [strongSelf.database incrementSoftFailureCountForEventType:eventName];
383 }
384 else if (class == SFAnalyticsEventClassSuccess || class == SFAnalyticsEventClassNote) {
385 [strongSelf.database incrementSuccessCountForEventType:eventName];
386 }
387 });
388 }
389
390 - (NSDictionary*)eventDictForEventName:(NSString*)eventName withAttributes:(NSDictionary*)attributes eventClass:(SFAnalyticsEventClass)eventClass
391 {
392 NSMutableDictionary* eventDict = attributes ? attributes.mutableCopy : [NSMutableDictionary dictionary];
393 eventDict[SFAnalyticsEventType] = eventName;
394 // our backend wants timestamps in milliseconds
395 eventDict[SFAnalyticsEventTime] = @([[NSDate date] timeIntervalSince1970] * 1000);
396 eventDict[SFAnalyticsEventClassKey] = @(eventClass);
397 [SFAnalytics addOSVersionToEvent:eventDict];
398
399 return eventDict;
400 }
401
402 // MARK: Sampling
403
404 - (SFAnalyticsSampler*)addMetricSamplerForName:(NSString *)samplerName withTimeInterval:(NSTimeInterval)timeInterval block:(NSNumber *(^)(void))block
405 {
406 if (!samplerName) {
407 secerror("SFAnalytics: cannot add sampler without name");
408 return nil;
409 }
410 if (timeInterval < 1.0f && timeInterval != SFAnalyticsSamplerIntervalOncePerReport) {
411 secerror("SFAnalytics: cannot add sampler with interval %f", timeInterval);
412 return nil;
413 }
414 if (!block) {
415 secerror("SFAnalytics: cannot add sampler without block");
416 return nil;
417 }
418
419 __block SFAnalyticsSampler* sampler = nil;
420
421 __weak __typeof(self) weakSelf = self;
422 dispatch_sync(_queue, ^{
423 __strong __typeof(self) strongSelf = weakSelf;
424 if (strongSelf->_samplers[samplerName]) {
425 secerror("SFAnalytics: sampler \"%@\" already exists", samplerName);
426 } else {
427 sampler = [[SFAnalyticsSampler alloc] initWithName:samplerName interval:timeInterval block:block clientClass:[self class]];
428 strongSelf->_samplers[samplerName] = sampler; // If sampler did not init because of bad data this 'removes' it from the dict, so a noop
429 }
430 });
431
432 return sampler;
433 }
434
435 - (SFAnalyticsMultiSampler*)AddMultiSamplerForName:(NSString *)samplerName withTimeInterval:(NSTimeInterval)timeInterval block:(NSDictionary<NSString *,NSNumber *> *(^)(void))block
436 {
437 if (!samplerName) {
438 secerror("SFAnalytics: cannot add sampler without name");
439 return nil;
440 }
441 if (timeInterval < 1.0f && timeInterval != SFAnalyticsSamplerIntervalOncePerReport) {
442 secerror("SFAnalytics: cannot add sampler with interval %f", timeInterval);
443 return nil;
444 }
445 if (!block) {
446 secerror("SFAnalytics: cannot add sampler without block");
447 return nil;
448 }
449
450 __block SFAnalyticsMultiSampler* sampler = nil;
451 __weak __typeof(self) weakSelf = self;
452 dispatch_sync(_queue, ^{
453 __strong __typeof(self) strongSelf = weakSelf;
454 if (strongSelf->_multisamplers[samplerName]) {
455 secerror("SFAnalytics: multisampler \"%@\" already exists", samplerName);
456 } else {
457 sampler = [[SFAnalyticsMultiSampler alloc] initWithName:samplerName interval:timeInterval block:block clientClass:[self class]];
458 strongSelf->_multisamplers[samplerName] = sampler;
459 }
460
461 });
462
463 return sampler;
464 }
465
466 - (SFAnalyticsSampler*)existingMetricSamplerForName:(NSString *)samplerName
467 {
468 __block SFAnalyticsSampler* sampler = nil;
469
470 __weak __typeof(self) weakSelf = self;
471 dispatch_sync(_queue, ^{
472 __strong __typeof(self) strongSelf = weakSelf;
473 if (strongSelf) {
474 sampler = strongSelf->_samplers[samplerName];
475 }
476 });
477 return sampler;
478 }
479
480 - (SFAnalyticsMultiSampler*)existingMultiSamplerForName:(NSString *)samplerName
481 {
482 __block SFAnalyticsMultiSampler* sampler = nil;
483
484 __weak __typeof(self) weakSelf = self;
485 dispatch_sync(_queue, ^{
486 __strong __typeof(self) strongSelf = weakSelf;
487 if (strongSelf) {
488 sampler = strongSelf->_multisamplers[samplerName];
489 }
490 });
491 return sampler;
492 }
493
494 - (void)removeMetricSamplerForName:(NSString *)samplerName
495 {
496 if (!samplerName) {
497 secerror("Attempt to remove sampler without specifying samplerName");
498 return;
499 }
500
501 __weak __typeof(self) weakSelf = self;
502 dispatch_async(_queue, ^{
503 __strong __typeof(self) strongSelf = weakSelf;
504 if (strongSelf) {
505 [strongSelf->_samplers[samplerName] pauseSampling]; // when dealloced it would also stop, but we're not sure when that is so let's stop it right away
506 [strongSelf->_samplers removeObjectForKey:samplerName];
507 }
508 });
509 }
510
511 - (void)removeMultiSamplerForName:(NSString *)samplerName
512 {
513 if (!samplerName) {
514 secerror("Attempt to remove multisampler without specifying samplerName");
515 return;
516 }
517
518 __weak __typeof(self) weakSelf = self;
519 dispatch_async(_queue, ^{
520 __strong __typeof(self) strongSelf = weakSelf;
521 if (strongSelf) {
522 [strongSelf->_multisamplers[samplerName] pauseSampling]; // when dealloced it would also stop, but we're not sure when that is so let's stop it right away
523 [strongSelf->_multisamplers removeObjectForKey:samplerName];
524 }
525 });
526 }
527
528 - (SFAnalyticsActivityTracker*)logSystemMetricsForActivityNamed:(NSString *)eventName withAction:(void (^)(void))action
529 {
530 if (![eventName isKindOfClass:[NSString class]]) {
531 secerror("Cannot log system metrics without name");
532 return nil;
533 }
534 SFAnalyticsActivityTracker* tracker = [[SFAnalyticsActivityTracker alloc] initWithName:eventName clientClass:[self class]];
535 if (action) {
536 [tracker performAction:action];
537 }
538 return tracker;
539 }
540
541 - (void)logMetric:(NSNumber *)metric withName:(NSString *)metricName
542 {
543 [self logMetric:metric withName:metricName oncePerReport:NO];
544 }
545
546 - (void)logMetric:(NSNumber*)metric withName:(NSString*)metricName oncePerReport:(BOOL)once
547 {
548 if (![metric isKindOfClass:[NSNumber class]] || ![metricName isKindOfClass:[NSString class]]) {
549 secerror("SFAnalytics: Need a valid result and name to log result");
550 return;
551 }
552
553 __weak __typeof(self) weakSelf = self;
554 dispatch_async(_queue, ^{
555 __strong __typeof(self) strongSelf = weakSelf;
556 if (strongSelf && !strongSelf->_disableLogging) {
557 if (once) {
558 [strongSelf.database removeAllSamplesForName:metricName];
559 }
560 [strongSelf.database addSample:metric forName:metricName];
561 }
562 });
563 }
564
565 @end
566
567 #endif // __OBJC2__