]> git.saurik.com Git - apple/security.git/blob - Analytics/Clients/LocalKeychainAnalytics.m
Security-59306.101.1.tar.gz
[apple/security.git] / Analytics / Clients / LocalKeychainAnalytics.m
1 #include "LocalKeychainAnalytics.h"
2
3 #import "Security/SFAnalyticsDefines.h"
4
5 #include <sys/stat.h>
6 #include <notify.h>
7
8 #include <utilities/SecFileLocations.h>
9 #include <utilities/SecAKSWrappers.h>
10
11 @interface LKAUpgradeOutcomeReport : NSObject
12 @property LKAKeychainUpgradeOutcome outcome;
13 @property NSDictionary* attributes;
14 - (instancetype) initWithOutcome:(LKAKeychainUpgradeOutcome)outcome attributes:(NSDictionary*)attributes;
15 @end
16
17 @implementation LKAUpgradeOutcomeReport
18 - (instancetype) initWithOutcome:(LKAKeychainUpgradeOutcome)outcome attributes:(NSDictionary*)attributes {
19 if (self = [super init]) {
20 self.outcome = outcome;
21 self.attributes = attributes;
22 }
23 return self;
24 }
25 @end
26
27 // Approved event types
28 // rdar://problem/41745059 SFAnalytics: collect keychain upgrade outcome information
29 LKAnalyticsFailableEvent const LKAEventUpgrade = (LKAnalyticsFailableEvent)@"LKAEventUpgrade";
30
31 // <rdar://problem/52038208> SFAnalytics: collect keychain backup success rates and duration
32 LKAnalyticsFailableEvent const LKAEventBackup = (LKAnalyticsFailableEvent)@"LKAEventBackup";
33 LKAnalyticsMetric const LKAMetricBackupDuration = (LKAnalyticsMetric)@"LKAMetricBackupDuration";
34
35 // Internal consts
36 NSString* const LKAOldSchemaKey = @"oldschema";
37 NSString* const LKANewSchemaKey = @"newschema";
38 NSString* const LKAUpgradeOutcomeKey = @"upgradeoutcome";
39 NSString* const LKABackupLastSuccessDate = @"backupLastSuccess";
40
41 @implementation LocalKeychainAnalytics {
42 BOOL _probablyInClassD;
43 NSMutableArray<LKAUpgradeOutcomeReport*>* _pendingReports;
44 dispatch_queue_t _queue;
45 int _notificationToken;
46 NSDate* _backupStartTime;
47 LKAKeychainBackupType _backupType;
48 }
49
50 - (instancetype __nullable)init {
51 if (self = [super init]) {
52 _probablyInClassD = YES;
53 _pendingReports = [NSMutableArray<LKAUpgradeOutcomeReport*> new];
54 _queue = dispatch_queue_create("LKADataQueue", DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL);
55 _notificationToken = NOTIFY_TOKEN_INVALID;
56 }
57 return self;
58 }
59
60 + (NSString*)databasePath {
61 return [self defaultAnalyticsDatabasePath:@"localkeychain"];
62 }
63
64 // MARK: Client-specific functionality
65
66 - (BOOL)canPersistMetrics {
67 @synchronized(self) {
68 if (!_probablyInClassD) {
69 return YES;
70 }
71 }
72
73 // If this gets busy we should start caching if AKS tells us no
74 bool hasBeenUnlocked = false;
75 if (!SecAKSGetHasBeenUnlocked(&hasBeenUnlocked, NULL) || !hasBeenUnlocked) {
76 static dispatch_once_t onceToken;
77 dispatch_once(&onceToken, ^{
78 notify_register_dispatch(kUserKeybagStateChangeNotification, &self->_notificationToken, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(int token) {
79 // For side effect of processing pending messages if out of class D
80 [self canPersistMetrics];
81 });
82 });
83 return NO;
84 }
85
86 @synchronized(self) {
87 _probablyInClassD = NO;
88 if (_notificationToken != NOTIFY_TOKEN_INVALID) {
89 notify_cancel(_notificationToken);
90 }
91 }
92
93 [self processPendingMessages];
94 return YES;
95 }
96
97 - (void)processPendingMessages {
98 dispatch_async(_queue, ^{
99 for (LKAUpgradeOutcomeReport* report in self->_pendingReports) {
100 [self reportKeychainUpgradeOutcome:report.outcome attributes:report.attributes];
101 }
102 });
103 }
104
105 - (void)reportKeychainUpgradeFrom:(int)oldVersion to:(int)newVersion outcome:(LKAKeychainUpgradeOutcome)outcome error:(NSError*)error {
106
107 NSMutableDictionary* attributes = [@{LKAOldSchemaKey : @(oldVersion),
108 LKANewSchemaKey : @(newVersion),
109 LKAUpgradeOutcomeKey : @(outcome),
110 } mutableCopy];
111 if (error) {
112 [attributes addEntriesFromDictionary:@{SFAnalyticsAttributeErrorDomain : error.domain,
113 SFAnalyticsAttributeErrorCode : @(error.code)}];
114 }
115
116 if (![self canPersistMetrics]) {
117 dispatch_async(_queue, ^{
118 [self->_pendingReports addObject:[[LKAUpgradeOutcomeReport alloc] initWithOutcome:outcome attributes:attributes]];
119 });
120 } else {
121 [self reportKeychainUpgradeOutcome:outcome attributes:attributes];
122 }
123 }
124
125 - (void)reportKeychainUpgradeOutcome:(LKAKeychainUpgradeOutcome)outcome attributes:(NSDictionary*)attributes {
126 if (outcome == LKAKeychainUpgradeOutcomeSuccess) {
127 [self logSuccessForEventNamed:LKAEventUpgrade];
128 } else {
129 // I could try and pick out the recoverable errors but I think we're good treating these all the same
130 [self logHardFailureForEventNamed:LKAEventUpgrade withAttributes:attributes];
131 }
132 }
133
134 - (void)reportKeychainBackupStartWithType:(LKAKeychainBackupType)type {
135 _backupStartTime = [NSDate date];
136 _backupType = type;
137 }
138
139 // Don't attempt to add to pending reports, this should not happen in Class D
140 - (void)reportKeychainBackupEnd:(bool)hasBackup error:(NSError*)error {
141 NSDate* backupEndTime = [NSDate date];
142
143 // Get duration in milliseconds rounded to 100ms.
144 NSInteger backupDuration = (int)(([backupEndTime timeIntervalSinceDate:_backupStartTime] + 0.05) * 10) * 100;
145
146 // Generate statistics on backup duration separately so we know what the situation is in the field even when succeeding
147 [self logMetric:@(backupDuration) withName:LKAMetricBackupDuration];
148
149 if (hasBackup) {
150 [self setDateProperty:backupEndTime forKey:LKABackupLastSuccessDate];
151 [self logSuccessForEventNamed:LKAEventBackup timestampBucket:SFAnalyticsTimestampBucketHour];
152 } else {
153 NSInteger daysSinceSuccess = [SFAnalytics fuzzyDaysSinceDate:[self datePropertyForKey:LKABackupLastSuccessDate]];
154 [self logResultForEvent:LKAEventBackup
155 hardFailure:YES
156 result:error
157 withAttributes:@{@"daysSinceSuccess" : @(daysSinceSuccess),
158 @"duration" : @(backupDuration),
159 @"type" : @(_backupType),
160 }
161 timestampBucket:SFAnalyticsTimestampBucketHour];
162 }
163 }
164
165 @end
166
167 // MARK: C Bridging
168
169 void LKAReportKeychainUpgradeOutcome(int fromversion, int toversion, LKAKeychainUpgradeOutcome outcome) {
170 @autoreleasepool {
171 [[LocalKeychainAnalytics logger] reportKeychainUpgradeFrom:fromversion to:toversion outcome:outcome error:NULL];
172 }
173 }
174
175 void LKAReportKeychainUpgradeOutcomeWithError(int fromversion, int toversion, LKAKeychainUpgradeOutcome outcome, CFErrorRef error) {
176 @autoreleasepool {
177 [[LocalKeychainAnalytics logger] reportKeychainUpgradeFrom:fromversion to:toversion outcome:outcome error:(__bridge NSError*)error];
178 }
179 }
180
181 void LKABackupReportStart(bool hasKeybag, bool hasPasscode, bool isEMCS) {
182 LKAKeychainBackupType type;
183 if (isEMCS) {
184 type = LKAKeychainBackupTypeEMCS;
185 } else if (hasKeybag && hasPasscode) {
186 type = LKAKeychainBackupTypeBagAndCode;
187 } else if (hasKeybag) {
188 type = LKAKeychainBackupTypeBag;
189 } else if (hasPasscode) {
190 type = LKAKeychainBackupTypeCode;
191 } else {
192 type = LKAKeychainBackupTypeNeither;
193 }
194
195 // Keep track of backup type and start time
196 @autoreleasepool {
197 [[LocalKeychainAnalytics logger] reportKeychainBackupStartWithType:type];
198 }
199 }
200
201 void LKABackupReportEnd(bool hasBackup, CFErrorRef error) {
202 @autoreleasepool {
203 [[LocalKeychainAnalytics logger] reportKeychainBackupEnd:hasBackup error:(__bridge NSError*)error];
204 }
205 }