2 * Copyright (c) 2018 Apple Inc. All Rights Reserved.
4 * @APPLE_LICENSE_HEADER_START@
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
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.
21 * @APPLE_LICENSE_HEADER_END@
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>
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"
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";
45 #define kCKRampManagerDefaultRetryTimeInSeconds 86400
48 @interface OTRamp (lockstateTracker) <CKKSLockStateNotification>
53 @property (nonatomic, strong) CKContainer *container;
54 @property (nonatomic, strong) CKDatabase *database;
56 @property (nonatomic, strong) CKRecordZone *zone;
57 @property (nonatomic, strong) CKRecordZoneID *zoneID;
59 @property (nonatomic, strong) NSString *recordName;
60 @property (nonatomic, strong) NSString *localSettingName;
61 @property (nonatomic, strong) CKRecordID *recordID;
63 @property (nonatomic, strong) CKKSAccountStateTracker *accountTracker;
64 @property (nonatomic, strong) CKKSLockStateTracker *lockStateTracker;
65 @property (nonatomic, strong) CKKSReachabilityTracker *reachabilityTracker;
67 @property CKKSAccountStatus accountStatus;
69 @property (readonly) Class<CKKSFetchRecordsOperation> fetchRecordRecordsOperationClass;
71 @property (atomic, strong) NSDate *lastFetch;
72 @property (atomic) NSTimeInterval retryAfter;
73 @property (atomic) BOOL cachedFeatureAllowed;
77 @implementation OTRamp
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
90 if ((self = [super init])) {
91 _container = container;
92 _recordName = [recordName copy];
93 _localSettingName = [localSettingName copy];
96 _accountTracker = accountTracker;
97 _lockStateTracker = lockStateTracker;
98 _reachabilityTracker = reachabilityTracker;
99 _fetchRecordRecordsOperationClass = fetchRecordRecordsOperationClass;
100 _lastFetch = [NSDate distantPast];
101 _retryAfter = kCKRampManagerDefaultRetryTimeInSeconds;
102 _cachedFeatureAllowed = NO;
107 -(void)fetchRampRecord:(CKOperationDiscretionaryNetworkBehavior)networkBehavior reply:(void (^)(BOOL featureAllowed, BOOL featurePromoted, BOOL featureVisible, NSInteger retryAfter, NSError *rampStateFetchError))recordRampStateFetchCompletionBlock
109 __weak __typeof(self) weakSelf = self;
111 CKOperationConfiguration *opConfig = [[CKOperationConfiguration alloc] init];
112 opConfig.allowsCellularAccess = YES;
113 opConfig.discretionaryNetworkBehavior = networkBehavior;
114 opConfig.isCloudKitSupportOperation = YES;
116 _recordID = [[CKRecordID alloc] initWithRecordName:_recordName zoneID:_zoneID];
117 CKFetchRecordsOperation *operation = [[[self.fetchRecordRecordsOperationClass class] alloc] initWithRecordIDs:@[ _recordID]];
119 operation.desiredKeys = @[kFeatureAllowedKey, kFeaturePromotedKey, kFeatureVisibleKey, kRetryAfterKey];
121 operation.configuration = opConfig;
122 operation.fetchRecordsCompletionBlock = ^(NSDictionary<CKRecordID *,CKRecord *> * _Nullable recordsByRecordID, NSError * _Nullable operationError) {
123 __strong __typeof(weakSelf) strongSelf = weakSelf;
125 secnotice("octagon", "received callback for released object");
126 operationError = [NSError errorWithDomain:OctagonErrorDomain code:OTErrorCKCallback userInfo:@{NSLocalizedDescriptionKey: @"Received callback for released object"}];
127 recordRampStateFetchCompletionBlock(NO, NO, NO, kCKRampManagerDefaultRetryTimeInSeconds , operationError);
131 BOOL featureAllowed = NO;
132 BOOL featurePromoted = NO;
133 BOOL featureVisible = NO;
134 NSInteger retryAfter = kCKRampManagerDefaultRetryTimeInSeconds;
136 secnotice("octagon", "Fetch operation records %@ fetchError %@", recordsByRecordID, operationError);
137 // There should only be only one record.
138 CKRecord *rampRecord = recordsByRecordID[strongSelf.recordID];
141 featureAllowed = [rampRecord[kFeatureAllowedKey] boolValue];
142 featurePromoted = [rampRecord[kFeaturePromotedKey] boolValue];
143 featureVisible = [rampRecord[kFeatureVisibleKey] boolValue];
144 retryAfter = [rampRecord[kRetryAfterKey] integerValue];
146 secnotice("octagon", "Fetch ramp state - featureAllowed %@, featurePromoted: %@, featureVisible: %@, retryAfter: %ld", (featureAllowed ? @YES : @NO), (featurePromoted ? @YES : @NO), (featureVisible ? @YES : @NO), (long)retryAfter);
148 secerror("octagon: Couldn't find CKRecord for ramp. Defaulting to not ramped in");
149 operationError = [NSError errorWithDomain:OctagonErrorDomain code:OTErrorRecordNotFound userInfo:@{NSLocalizedDescriptionKey: @" Couldn't find CKRecord for ramp. Defaulting to not ramped in"}];
151 recordRampStateFetchCompletionBlock(featureAllowed, featurePromoted, featureVisible, retryAfter, operationError);
154 [self.database addOperation: operation];
155 secnotice("octagon", "Attempting to fetch ramp state from CloudKit");
158 -(BOOL) checkRampStateWithError:(NSError**)error
160 __block BOOL isFeatureEnabled = NO;
161 __block NSError* localError = nil;
162 __block NSInteger localRetryAfter = 0;
164 //defaults write to for whether or not a ramp record returns "enabled or disabled"
165 CFBooleanRef enabled = (CFBooleanRef)CFPreferencesCopyValue((__bridge CFStringRef)self.localSettingName,
166 CFSTR("com.apple.security"),
167 kCFPreferencesCurrentUser, kCFPreferencesAnyHost);
169 secnotice("octagon", "%@ Defaults availability: SecCKKSTestsEnabled[%s] DefaultsPointer[%s] DefaultsValue[%s]", (__bridge CFStringRef)self.localSettingName,
170 SecCKKSTestsEnabled() ? "True": "False", (enabled != NULL) ? "True": "False",
171 (enabled && (CFGetTypeID(enabled) == CFBooleanGetTypeID()) && (enabled == kCFBooleanTrue)) ? "True": "False");
173 if(!SecCKKSTestsEnabled() && enabled && CFGetTypeID(enabled) == CFBooleanGetTypeID()){
174 BOOL localConfigEnable = (enabled == kCFBooleanTrue);
175 secnotice("octagon", "feature is %@: %@ (local config)", localConfigEnable ? @"enabled" : @"disabled", self.recordName);
176 CFReleaseNull(enabled);
177 return localConfigEnable;
179 CFReleaseNull(enabled);
181 NSDate* now = [[NSDate alloc] init];
183 if([now timeIntervalSinceDate: self.lastFetch] < self.retryAfter) {
184 return self.cachedFeatureAllowed;
187 if(self.lockStateTracker.isLocked){
188 secnotice("octagon","device is locked! can't check ramp state");
189 localError = [NSError errorWithDomain:(__bridge NSString*)kSecErrorDomain
190 code:errSecInteractionNotAllowed
191 userInfo:@{NSLocalizedDescriptionKey: @"device is locked"}];
198 // Wait until the account tracker has had a chance to figure out the state
199 [self.accountTracker.ckAccountInfoInitialized wait:5*NSEC_PER_SEC];
200 if(self.accountTracker.currentCKAccountInfo.accountStatus != CKAccountStatusAvailable){
201 secnotice("octagon","not signed in! can't check ramp state");
202 localError = [NSError errorWithDomain:OctagonErrorDomain
203 code:OTErrorNotSignedIn
204 userInfo:@{NSLocalizedDescriptionKey: @"not signed in"}];
210 if(!self.reachabilityTracker.currentReachability){
211 secnotice("octagon","no network! can't check ramp state");
212 localError = [NSError errorWithDomain:OctagonErrorDomain
213 code:OTErrorNoNetwork
214 userInfo:@{NSLocalizedDescriptionKey: @"no network"}];
221 CKKSAnalytics* logger = [CKKSAnalytics logger];
222 SFAnalyticsActivityTracker *tracker = [logger logSystemMetricsForActivityNamed:CKKSActivityOTFetchRampState withAction:nil];
224 dispatch_semaphore_t sema = dispatch_semaphore_create(0);
228 [self fetchRampRecord:CKOperationDiscretionaryNetworkBehaviorNonDiscretionary reply:^(BOOL featureAllowed, BOOL featurePromoted, BOOL featureVisible, NSInteger retryAfter, NSError *rampStateFetchError) {
229 secnotice("octagon", "fetch ramp records returned with featureAllowed: %d,\n featurePromoted: %d,\n featureVisible: %d,\n", featureAllowed, featurePromoted, featureVisible);
231 isFeatureEnabled = featureAllowed;
232 localRetryAfter = retryAfter;
233 if(rampStateFetchError){
234 localError = rampStateFetchError;
236 dispatch_semaphore_signal(sema);
239 int64_t timeout = (int64_t)(SecCKKSTestsEnabled() ? 2*NSEC_PER_SEC : NSEC_PER_SEC * 65);
240 if(dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, timeout)) != 0) {
241 secnotice("octagon", "timed out waiting for response from CloudKit\n");
242 localError = [NSError errorWithDomain:OctagonErrorDomain code:OTErrorCKTimeOut userInfo:@{NSLocalizedDescriptionKey: @"timed out waiting for response from CloudKit"}];
244 [logger logUnrecoverableError:localError forEvent:OctagonEventRamp withAttributes:@{
245 OctagonEventAttributeFailureReason : @"cloud kit timed out"}
251 if(localRetryAfter > 0){
252 secnotice("octagon", "cloud kit asked security to retry: %lu", (unsigned long)localRetryAfter);
253 self.retryAfter = localRetryAfter;
257 secerror("octagon: had an error fetching ramp state: %@", localError);
258 [logger logUnrecoverableError:localError forEvent:OctagonEventRamp withAttributes:@{
259 OctagonEventAttributeFailureReason : @"fetching ramp state"}
265 if(isFeatureEnabled){
266 [logger logSuccessForEventNamed:OctagonEventRamp];
269 self.lastFetch = now;
270 self.cachedFeatureAllowed = isFeatureEnabled;
271 return isFeatureEnabled;