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";
36 NSString* const LKAOldSchemaKey = @"oldschema";
37 NSString* const LKANewSchemaKey = @"newschema";
38 NSString* const LKAUpgradeOutcomeKey = @"upgradeoutcome";
39 NSString* const LKABackupLastSuccessDate = @"backupLastSuccess";
41 @implementation LocalKeychainAnalytics {
42 BOOL _probablyInClassD;
43 NSMutableArray<LKAUpgradeOutcomeReport*>* _pendingReports;
44 dispatch_queue_t _queue;
45 int _notificationToken;
46 NSDate* _backupStartTime;
47 LKAKeychainBackupType _backupType;
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;
60 + (NSString*)databasePath {
61 return [self defaultAnalyticsDatabasePath:@"localkeychain"];
64 // MARK: Client-specific functionality
66 - (BOOL)canPersistMetrics {
68 if (!_probablyInClassD) {
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];
87 _probablyInClassD = NO;
88 if (_notificationToken != NOTIFY_TOKEN_INVALID) {
89 notify_cancel(_notificationToken);
93 [self processPendingMessages];
97 - (void)processPendingMessages {
98 dispatch_async(_queue, ^{
99 for (LKAUpgradeOutcomeReport* report in self->_pendingReports) {
100 [self reportKeychainUpgradeOutcome:report.outcome attributes:report.attributes];
105 - (void)reportKeychainUpgradeFrom:(int)oldVersion to:(int)newVersion outcome:(LKAKeychainUpgradeOutcome)outcome error:(NSError*)error {
107 NSMutableDictionary* attributes = [@{LKAOldSchemaKey : @(oldVersion),
108 LKANewSchemaKey : @(newVersion),
109 LKAUpgradeOutcomeKey : @(outcome),
112 [attributes addEntriesFromDictionary:@{SFAnalyticsAttributeErrorDomain : error.domain,
113 SFAnalyticsAttributeErrorCode : @(error.code)}];
116 if (![self canPersistMetrics]) {
117 dispatch_async(_queue, ^{
118 [self->_pendingReports addObject:[[LKAUpgradeOutcomeReport alloc] initWithOutcome:outcome attributes:attributes]];
121 [self reportKeychainUpgradeOutcome:outcome attributes:attributes];
125 - (void)reportKeychainUpgradeOutcome:(LKAKeychainUpgradeOutcome)outcome attributes:(NSDictionary*)attributes {
126 if (outcome == LKAKeychainUpgradeOutcomeSuccess) {
127 [self logSuccessForEventNamed:LKAEventUpgrade];
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];
134 - (void)reportKeychainBackupStartWithType:(LKAKeychainBackupType)type {
135 _backupStartTime = [NSDate date];
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];
143 // Get duration in milliseconds rounded to 100ms.
144 NSInteger backupDuration = (int)(([backupEndTime timeIntervalSinceDate:_backupStartTime] + 0.05) * 10) * 100;
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];
150 [self setDateProperty:backupEndTime forKey:LKABackupLastSuccessDate];
151 [self logSuccessForEventNamed:LKAEventBackup timestampBucket:SFAnalyticsTimestampBucketHour];
153 NSInteger daysSinceSuccess = [SFAnalytics fuzzyDaysSinceDate:[self datePropertyForKey:LKABackupLastSuccessDate]];
154 [self logResultForEvent:LKAEventBackup
157 withAttributes:@{@"daysSinceSuccess" : @(daysSinceSuccess),
158 @"duration" : @(backupDuration),
159 @"type" : @(_backupType),
161 timestampBucket:SFAnalyticsTimestampBucketHour];
169 void LKAReportKeychainUpgradeOutcome(int fromversion, int toversion, LKAKeychainUpgradeOutcome outcome) {
171 [[LocalKeychainAnalytics logger] reportKeychainUpgradeFrom:fromversion to:toversion outcome:outcome error:NULL];
175 void LKAReportKeychainUpgradeOutcomeWithError(int fromversion, int toversion, LKAKeychainUpgradeOutcome outcome, CFErrorRef error) {
177 [[LocalKeychainAnalytics logger] reportKeychainUpgradeFrom:fromversion to:toversion outcome:outcome error:(__bridge NSError*)error];
181 void LKABackupReportStart(bool hasKeybag, bool hasPasscode, bool isEMCS) {
182 LKAKeychainBackupType type;
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;
192 type = LKAKeychainBackupTypeNeither;
195 // Keep track of backup type and start time
197 [[LocalKeychainAnalytics logger] reportKeychainBackupStartWithType:type];
201 void LKABackupReportEnd(bool hasBackup, CFErrorRef error) {
203 [[LocalKeychainAnalytics logger] reportKeychainBackupEnd:hasBackup error:(__bridge NSError*)error];