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