]> git.saurik.com Git - apple/security.git/blob - keychain/ot/OTRamping.m
Security-59306.11.20.tar.gz
[apple/security.git] / keychain / ot / OTRamping.m
1 /*
2 * Copyright (c) 2018 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 OCTAGON
25
26 #import <Foundation/Foundation.h>
27 #import <CoreFoundation/CoreFoundation.h>
28 #import <CloudKit/CloudKit.h>
29 #import <CloudKit/CKContainer_Private.h>
30 #import <utilities/debugging.h>
31 #import "OTRamping.h"
32 #import "keychain/ckks/CKKS.h"
33 #import "keychain/ckks/CKKSNearFutureScheduler.h"
34 #import "keychain/ckks/CKKSAnalytics.h"
35 #import "keychain/ot/OTDefines.h"
36 #import "keychain/ot/OTConstants.h"
37 #import "keychain/ckks/CKKS.h"
38
39 static NSString* kFeatureAllowedKey = @"FeatureAllowed";
40 static NSString* kFeaturePromotedKey = @"FeaturePromoted";
41 static NSString* kFeatureVisibleKey = @"FeatureVisible";
42 static NSString* kRetryAfterKey = @"RetryAfter";
43 static NSString* kRampPriorityKey = @"RampPriority";
44
45 #define kCKRampManagerDefaultRetryTimeInSeconds 86400
46
47 #if OCTAGON
48 @interface OTRamp (lockstateTracker) <CKKSLockStateNotification>
49 @end
50 #endif
51
52 @interface OTRamp ()
53 @property (nonatomic, strong) CKContainer *container;
54 @property (nonatomic, strong) CKDatabase *database;
55
56 @property (nonatomic, strong) CKRecordZone *zone;
57 @property (nonatomic, strong) CKRecordZoneID *zoneID;
58
59 @property (nonatomic, strong) NSString *recordName;
60 @property (nonatomic, strong) NSString *localSettingName;
61 @property (nonatomic, strong) CKRecordID *recordID;
62
63 @property (nonatomic, strong) CKKSAccountStateTracker *accountTracker;
64 @property (nonatomic, strong) CKKSLockStateTracker *lockStateTracker;
65 @property (nonatomic, strong) CKKSReachabilityTracker *reachabilityTracker;
66
67 @property CKKSAccountStatus accountStatus;
68
69 @property (readonly) Class<CKKSFetchRecordsOperation> fetchRecordRecordsOperationClass;
70
71 @property (atomic, strong) NSDate *lastFetch;
72 @property (atomic) NSTimeInterval retryAfter;
73 @property (atomic) BOOL cachedFeatureAllowed;
74
75 @end
76
77 @implementation OTRamp
78
79 -(instancetype) initWithRecordName:(NSString *) recordName
80 localSettingName:(NSString*) localSettingName
81 container:(CKContainer*) container
82 database:(CKDatabase*) database
83 zoneID:(CKRecordZoneID*) zoneID
84 accountTracker:(CKKSAccountStateTracker*) accountTracker
85 lockStateTracker:(CKKSLockStateTracker*) lockStateTracker
86 reachabilityTracker:(CKKSReachabilityTracker*) reachabilityTracker
87 fetchRecordRecordsOperationClass:(Class<CKKSFetchRecordsOperation>) fetchRecordRecordsOperationClass
88
89 {
90 self = [super init];
91 if(self){
92 _container = container;
93 _recordName = [recordName copy];
94 _localSettingName = [localSettingName copy];
95 _database = database;
96 _zoneID = zoneID;
97 _accountTracker = accountTracker;
98 _lockStateTracker = lockStateTracker;
99 _reachabilityTracker = reachabilityTracker;
100 _fetchRecordRecordsOperationClass = fetchRecordRecordsOperationClass;
101 _lastFetch = [NSDate distantPast];
102 _retryAfter = kCKRampManagerDefaultRetryTimeInSeconds;
103 _cachedFeatureAllowed = NO;
104 }
105 return self;
106 }
107
108 -(void)fetchRampRecord:(CKOperationDiscretionaryNetworkBehavior)networkBehavior reply:(void (^)(BOOL featureAllowed, BOOL featurePromoted, BOOL featureVisible, NSInteger retryAfter, NSError *rampStateFetchError))recordRampStateFetchCompletionBlock
109 {
110 __weak __typeof(self) weakSelf = self;
111
112 CKOperationConfiguration *opConfig = [[CKOperationConfiguration alloc] init];
113 opConfig.allowsCellularAccess = YES;
114 opConfig.discretionaryNetworkBehavior = networkBehavior;
115 opConfig.isCloudKitSupportOperation = YES;
116
117 _recordID = [[CKRecordID alloc] initWithRecordName:_recordName zoneID:_zoneID];
118 CKFetchRecordsOperation *operation = [[[self.fetchRecordRecordsOperationClass class] alloc] initWithRecordIDs:@[ _recordID]];
119
120 operation.desiredKeys = @[kFeatureAllowedKey, kFeaturePromotedKey, kFeatureVisibleKey, kRetryAfterKey];
121
122 operation.configuration = opConfig;
123 operation.fetchRecordsCompletionBlock = ^(NSDictionary<CKRecordID *,CKRecord *> * _Nullable recordsByRecordID, NSError * _Nullable operationError) {
124 __strong __typeof(weakSelf) strongSelf = weakSelf;
125 if(!strongSelf) {
126 secnotice("octagon", "received callback for released object");
127 operationError = [NSError errorWithDomain:OctagonErrorDomain code:OTErrorCKCallback userInfo:@{NSLocalizedDescriptionKey: @"Received callback for released object"}];
128 recordRampStateFetchCompletionBlock(NO, NO, NO, kCKRampManagerDefaultRetryTimeInSeconds , operationError);
129 return;
130 }
131
132 BOOL featureAllowed = NO;
133 BOOL featurePromoted = NO;
134 BOOL featureVisible = NO;
135 NSInteger retryAfter = kCKRampManagerDefaultRetryTimeInSeconds;
136
137 secnotice("octagon", "Fetch operation records %@ fetchError %@", recordsByRecordID, operationError);
138 // There should only be only one record.
139 CKRecord *rampRecord = recordsByRecordID[strongSelf.recordID];
140
141 if (rampRecord) {
142 featureAllowed = [rampRecord[kFeatureAllowedKey] boolValue];
143 featurePromoted = [rampRecord[kFeaturePromotedKey] boolValue];
144 featureVisible = [rampRecord[kFeatureVisibleKey] boolValue];
145 retryAfter = [rampRecord[kRetryAfterKey] integerValue];
146
147 secnotice("octagon", "Fetch ramp state - featureAllowed %@, featurePromoted: %@, featureVisible: %@, retryAfter: %ld", (featureAllowed ? @YES : @NO), (featurePromoted ? @YES : @NO), (featureVisible ? @YES : @NO), (long)retryAfter);
148 } else {
149 secerror("octagon: Couldn't find CKRecord for ramp. Defaulting to not ramped in");
150 operationError = [NSError errorWithDomain:OctagonErrorDomain code:OTErrorRecordNotFound userInfo:@{NSLocalizedDescriptionKey: @" Couldn't find CKRecord for ramp. Defaulting to not ramped in"}];
151 }
152 recordRampStateFetchCompletionBlock(featureAllowed, featurePromoted, featureVisible, retryAfter, operationError);
153 };
154
155 [self.database addOperation: operation];
156 secnotice("octagon", "Attempting to fetch ramp state from CloudKit");
157 }
158
159 -(BOOL) checkRampStateWithError:(NSError**)error
160 {
161 __block BOOL isFeatureEnabled = NO;
162 __block NSError* localError = nil;
163 __block NSInteger localRetryAfter = 0;
164
165 //defaults write to for whether or not a ramp record returns "enabled or disabled"
166 CFBooleanRef enabled = (CFBooleanRef)CFPreferencesCopyValue((__bridge CFStringRef)self.localSettingName,
167 CFSTR("com.apple.security"),
168 kCFPreferencesCurrentUser, kCFPreferencesAnyHost);
169
170 secnotice("octagon", "%@ Defaults availability: SecCKKSTestsEnabled[%s] DefaultsPointer[%s] DefaultsValue[%s]", (__bridge CFStringRef)self.localSettingName,
171 SecCKKSTestsEnabled() ? "True": "False", (enabled != NULL) ? "True": "False",
172 (enabled && (CFGetTypeID(enabled) == CFBooleanGetTypeID()) && (enabled == kCFBooleanTrue)) ? "True": "False");
173
174 if(!SecCKKSTestsEnabled() && enabled && CFGetTypeID(enabled) == CFBooleanGetTypeID()){
175 BOOL localConfigEnable = (enabled == kCFBooleanTrue);
176 secnotice("octagon", "feature is %@: %@ (local config)", localConfigEnable ? @"enabled" : @"disabled", self.recordName);
177 CFReleaseNull(enabled);
178 return localConfigEnable;
179 }
180 CFReleaseNull(enabled);
181
182 NSDate* now = [[NSDate alloc] init];
183
184 if([now timeIntervalSinceDate: self.lastFetch] < self.retryAfter) {
185 return self.cachedFeatureAllowed;
186 }
187
188 if(self.lockStateTracker.isLocked){
189 secnotice("octagon","device is locked! can't check ramp state");
190 localError = [NSError errorWithDomain:(__bridge NSString*)kSecErrorDomain
191 code:errSecInteractionNotAllowed
192 userInfo:@{NSLocalizedDescriptionKey: @"device is locked"}];
193 if(error){
194 *error = localError;
195 }
196 return NO;
197 }
198
199 // Wait until the account tracker has had a chance to figure out the state
200 [self.accountTracker.ckAccountInfoInitialized wait:5*NSEC_PER_SEC];
201 if(self.accountTracker.currentCKAccountInfo.accountStatus != CKAccountStatusAvailable){
202 secnotice("octagon","not signed in! can't check ramp state");
203 localError = [NSError errorWithDomain:OctagonErrorDomain
204 code:OTErrorNotSignedIn
205 userInfo:@{NSLocalizedDescriptionKey: @"not signed in"}];
206 if(error){
207 *error = localError;
208 }
209 return NO;
210 }
211 if(!self.reachabilityTracker.currentReachability){
212 secnotice("octagon","no network! can't check ramp state");
213 localError = [NSError errorWithDomain:OctagonErrorDomain
214 code:OTErrorNoNetwork
215 userInfo:@{NSLocalizedDescriptionKey: @"no network"}];
216 if(error){
217 *error = localError;
218 }
219 return NO;
220 }
221
222 CKKSAnalytics* logger = [CKKSAnalytics logger];
223 SFAnalyticsActivityTracker *tracker = [logger logSystemMetricsForActivityNamed:CKKSActivityOTFetchRampState withAction:nil];
224
225 dispatch_semaphore_t sema = dispatch_semaphore_create(0);
226
227 [tracker start];
228
229 [self fetchRampRecord:CKOperationDiscretionaryNetworkBehaviorNonDiscretionary reply:^(BOOL featureAllowed, BOOL featurePromoted, BOOL featureVisible, NSInteger retryAfter, NSError *rampStateFetchError) {
230 secnotice("octagon", "fetch ramp records returned with featureAllowed: %d,\n featurePromoted: %d,\n featureVisible: %d,\n", featureAllowed, featurePromoted, featureVisible);
231
232 isFeatureEnabled = featureAllowed;
233 localRetryAfter = retryAfter;
234 if(rampStateFetchError){
235 localError = rampStateFetchError;
236 }
237 dispatch_semaphore_signal(sema);
238 }];
239
240 int64_t timeout = (int64_t)(SecCKKSTestsEnabled() ? 2*NSEC_PER_SEC : NSEC_PER_SEC * 65);
241 if(dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, timeout)) != 0) {
242 secnotice("octagon", "timed out waiting for response from CloudKit\n");
243 localError = [NSError errorWithDomain:OctagonErrorDomain code:OTErrorCKTimeOut userInfo:@{NSLocalizedDescriptionKey: @"timed out waiting for response from CloudKit"}];
244
245 [logger logUnrecoverableError:localError forEvent:OctagonEventRamp withAttributes:@{
246 OctagonEventAttributeFailureReason : @"cloud kit timed out"}
247 ];
248 }
249
250 [tracker stop];
251
252 if(localRetryAfter > 0){
253 secnotice("octagon", "cloud kit asked security to retry: %lu", (unsigned long)localRetryAfter);
254 self.retryAfter = localRetryAfter;
255 }
256
257 if(localError){
258 secerror("octagon: had an error fetching ramp state: %@", localError);
259 [logger logUnrecoverableError:localError forEvent:OctagonEventRamp withAttributes:@{
260 OctagonEventAttributeFailureReason : @"fetching ramp state"}
261 ];
262 if(error){
263 *error = localError;
264 }
265 }
266 if(isFeatureEnabled){
267 [logger logSuccessForEventNamed:OctagonEventRamp];
268 }
269
270 self.lastFetch = now;
271 self.cachedFeatureAllowed = isFeatureEnabled;
272 return isFeatureEnabled;
273 }
274
275 @end
276 #endif
277
278