1 #include "LocalKeychainAnalytics.h"
3 #import "Security/SFAnalyticsDefines.h"
8 #include <utilities/SecFileLocations.h>
9 #include <utilities/SecAKSWrappers.h>
11 @interface LKAUpgradeOutcomeReport : NSObject
12 @property LKAKeychainUpgradeOutcome outcome;
13 @property NSDictionary* attributes;
14 - (instancetype) initWithOutcome:(LKAKeychainUpgradeOutcome)outcome attributes:(NSDictionary*)attributes;
17 @implementation LKAUpgradeOutcomeReport
18 - (instancetype) initWithOutcome:(LKAKeychainUpgradeOutcome)outcome attributes:(NSDictionary*)attributes {
19 if (self = [super init]) {
20 self.outcome = outcome;
21 self.attributes = attributes;
27 // Approved event types
28 // rdar://problem/41745059 SFAnalytics: collect keychain upgrade outcome information
29 LKAnalyticsFailableEvent const LKAEventUpgrade = (LKAnalyticsFailableEvent)@"LKAEventUpgrade";
31 // <rdar://problem/52038208> SFAnalytics: collect keychain backup success rates and duration
32 LKAnalyticsFailableEvent const LKAEventBackup = (LKAnalyticsFailableEvent)@"LKAEventBackup";
33 LKAnalyticsMetric const LKAMetricBackupDuration = (LKAnalyticsMetric)@"LKAMetricBackupDuration";
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";
40 NSString* const LKAOldSchemaKey = @"oldschema";
41 NSString* const LKANewSchemaKey = @"newschema";
42 NSString* const LKAUpgradeOutcomeKey = @"upgradeoutcome";
43 NSString* const LKABackupLastSuccessDate = @"backupLastSuccess";
45 @implementation LocalKeychainAnalytics {
46 BOOL _probablyInClassD;
47 NSMutableArray<LKAUpgradeOutcomeReport*>* _pendingReports;
48 dispatch_queue_t _queue;
49 int _notificationToken;
50 NSDate* _backupStartTime;
51 LKAKeychainBackupType _backupType;
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;
64 + (NSString*)databasePath {
65 return [self defaultAnalyticsDatabasePath:@"localkeychain"];
68 // MARK: Client-specific functionality
70 - (BOOL)canPersistMetrics {
72 if (!_probablyInClassD) {
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];
91 _probablyInClassD = NO;
92 if (_notificationToken != NOTIFY_TOKEN_INVALID) {
93 notify_cancel(_notificationToken);
97 [self processPendingMessages];
101 - (void)processPendingMessages {
102 dispatch_async(_queue, ^{
103 for (LKAUpgradeOutcomeReport* report in self->_pendingReports) {
104 [self reportKeychainUpgradeOutcome:report.outcome attributes:report.attributes];
109 - (void)reportKeychainUpgradeFrom:(int)oldVersion to:(int)newVersion outcome:(LKAKeychainUpgradeOutcome)outcome error:(NSError*)error {
111 NSMutableDictionary* attributes = [@{LKAOldSchemaKey : @(oldVersion),
112 LKANewSchemaKey : @(newVersion),
113 LKAUpgradeOutcomeKey : @(outcome),
116 [attributes addEntriesFromDictionary:@{SFAnalyticsAttributeErrorDomain : error.domain,
117 SFAnalyticsAttributeErrorCode : @(error.code)}];
120 if (![self canPersistMetrics]) {
121 dispatch_async(_queue, ^{
122 [self->_pendingReports addObject:[[LKAUpgradeOutcomeReport alloc] initWithOutcome:outcome attributes:attributes]];
125 [self reportKeychainUpgradeOutcome:outcome attributes:attributes];
129 - (void)reportKeychainUpgradeOutcome:(LKAKeychainUpgradeOutcome)outcome attributes:(NSDictionary*)attributes {
130 if (outcome == LKAKeychainUpgradeOutcomeSuccess) {
131 [self logSuccessForEventNamed:LKAEventUpgrade];
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];
138 - (void)reportKeychainBackupStartWithType:(LKAKeychainBackupType)type {
139 _backupStartTime = [NSDate date];
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];
147 // Get duration in milliseconds rounded to 100ms.
148 NSInteger backupDuration = (int)(([backupEndTime timeIntervalSinceDate:_backupStartTime] + 0.05) * 10) * 100;
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];
154 [self setDateProperty:backupEndTime forKey:LKABackupLastSuccessDate];
155 [self logSuccessForEventNamed:LKAEventBackup timestampBucket:SFAnalyticsTimestampBucketHour];
157 NSInteger daysSinceSuccess = [SFAnalytics fuzzyDaysSinceDate:[self datePropertyForKey:LKABackupLastSuccessDate]];
159 // Backups fail all the time due to devices being locked. If a backup has happened recently,
160 // let's not even report it, to avoid crowding out more useful data
161 bool boringError = error.code == errSecInteractionNotAllowed && daysSinceSuccess == 0;
164 [self logResultForEvent:LKAEventBackup
167 withAttributes:@{@"daysSinceSuccess" : @(daysSinceSuccess),
168 @"duration" : @(backupDuration),
169 @"type" : @(_backupType),
171 timestampBucket:SFAnalyticsTimestampBucketHour];
180 void LKAReportKeychainUpgradeOutcome(int fromversion, int toversion, LKAKeychainUpgradeOutcome outcome) {
182 [[LocalKeychainAnalytics logger] reportKeychainUpgradeFrom:fromversion to:toversion outcome:outcome error:NULL];
186 void LKAReportKeychainUpgradeOutcomeWithError(int fromversion, int toversion, LKAKeychainUpgradeOutcome outcome, CFErrorRef error) {
188 [[LocalKeychainAnalytics logger] reportKeychainUpgradeFrom:fromversion to:toversion outcome:outcome error:(__bridge NSError*)error];
192 void LKABackupReportStart(bool hasKeybag, bool hasPasscode, bool isEMCS) {
193 LKAKeychainBackupType type;
195 type = LKAKeychainBackupTypeEMCS;
196 } else if (hasKeybag && hasPasscode) {
197 type = LKAKeychainBackupTypeBagAndCode;
198 } else if (hasKeybag) {
199 type = LKAKeychainBackupTypeBag;
200 } else if (hasPasscode) {
201 type = LKAKeychainBackupTypeCode;
203 type = LKAKeychainBackupTypeNeither;
206 // Keep track of backup type and start time
208 [[LocalKeychainAnalytics logger] reportKeychainBackupStartWithType:type];
212 void LKABackupReportEnd(bool hasBackup, CFErrorRef error) {
214 [[LocalKeychainAnalytics logger] reportKeychainBackupEnd:hasBackup error:(__bridge NSError*)error];