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"
37 static NSString* kFeatureAllowedKey = @"FeatureAllowed";
38 static NSString* kFeaturePromotedKey = @"FeaturePromoted";
39 static NSString* kFeatureVisibleKey = @"FeatureVisible";
40 static NSString* kRetryAfterKey = @"RetryAfter";
41 static NSString* kRampPriorityKey = @"RampPriority";
43 #define kCKRampManagerDefaultRetryTimeInSeconds 86400
46 @interface OTRamp (lockstateTracker) <CKKSLockStateNotification>
51 @property (nonatomic, strong) CKContainer *container;
52 @property (nonatomic, strong) CKDatabase *database;
54 @property (nonatomic, strong) CKRecordZone *zone;
55 @property (nonatomic, strong) CKRecordZoneID *zoneID;
57 @property (nonatomic, strong) NSString *recordName;
58 @property (nonatomic, strong) NSString *featureName;
59 @property (nonatomic, strong) CKRecordID *recordID;
61 @property (nonatomic, strong) CKKSCKAccountStateTracker *accountTracker;
62 @property (nonatomic, strong) CKKSLockStateTracker *lockStateTracker;
63 @property (nonatomic, strong) CKKSReachabilityTracker *reachabilityTracker;
65 @property CKKSAccountStatus accountStatus;
67 @property (readonly) Class<CKKSFetchRecordsOperation> fetchRecordRecordsOperationClass;
71 @implementation OTRamp
73 -(instancetype) initWithRecordName:(NSString *) recordName
74 featureName:(NSString*) featureName
75 container:(CKContainer*) container
76 database:(CKDatabase*) database
77 zoneID:(CKRecordZoneID*) zoneID
78 accountTracker:(CKKSCKAccountStateTracker*) accountTracker
79 lockStateTracker:(CKKSLockStateTracker*) lockStateTracker
80 reachabilityTracker:(CKKSReachabilityTracker*) reachabilityTracker
81 fetchRecordRecordsOperationClass:(Class<CKKSFetchRecordsOperation>) fetchRecordRecordsOperationClass
86 _container = container;
87 _recordName = [recordName copy];
88 _featureName = [featureName copy];
91 _accountTracker = accountTracker;
92 _lockStateTracker = lockStateTracker;
93 _reachabilityTracker = reachabilityTracker;
94 _fetchRecordRecordsOperationClass = fetchRecordRecordsOperationClass;
99 -(void) fetchRampRecord:(NSQualityOfService)qos reply:(void (^)(BOOL featureAllowed, BOOL featurePromoted, BOOL featureVisible, NSInteger retryAfter, NSError *rampStateFetchError))recordRampStateFetchCompletionBlock
101 __weak __typeof(self) weakSelf = self;
103 CKOperationConfiguration *opConfig = [[CKOperationConfiguration alloc] init];
104 opConfig.allowsCellularAccess = YES;
105 opConfig.qualityOfService = qos;
107 _recordID = [[CKRecordID alloc] initWithRecordName:_recordName zoneID:_zoneID];
108 CKFetchRecordsOperation *operation = [[[self.fetchRecordRecordsOperationClass class] alloc] initWithRecordIDs:@[ _recordID]];
110 operation.desiredKeys = @[kFeatureAllowedKey, kFeaturePromotedKey, kFeatureVisibleKey, kRetryAfterKey];
112 operation.configuration = opConfig;
113 operation.fetchRecordsCompletionBlock = ^(NSDictionary<CKRecordID *,CKRecord *> * _Nullable recordsByRecordID, NSError * _Nullable operationError) {
114 __strong __typeof(weakSelf) strongSelf = weakSelf;
116 secnotice("octagon", "received callback for released object");
117 operationError = [NSError errorWithDomain:octagonErrorDomain code:OTErrorCKCallback userInfo:@{NSLocalizedDescriptionKey: @"Received callback for released object"}];
118 recordRampStateFetchCompletionBlock(NO, NO, NO, kCKRampManagerDefaultRetryTimeInSeconds , operationError);
122 BOOL featureAllowed = NO;
123 BOOL featurePromoted = NO;
124 BOOL featureVisible = NO;
125 NSInteger retryAfter = kCKRampManagerDefaultRetryTimeInSeconds;
127 secnotice("octagon", "Fetch operation records %@ fetchError %@", recordsByRecordID, operationError);
128 // There should only be only one record.
129 CKRecord *rampRecord = recordsByRecordID[strongSelf.recordID];
132 featureAllowed = [rampRecord[kFeatureAllowedKey] boolValue];
133 featurePromoted = [rampRecord[kFeaturePromotedKey] boolValue];
134 featureVisible = [rampRecord[kFeatureVisibleKey] boolValue];
135 retryAfter = [rampRecord[kRetryAfterKey] integerValue];
137 secnotice("octagon", "Fetch ramp state - featureAllowed %@, featurePromoted: %@, featureVisible: %@, retryAfter: %ld", (featureAllowed ? @YES : @NO), (featurePromoted ? @YES : @NO), (featureVisible ? @YES : @NO), (long)retryAfter);
139 secerror("octagon: Couldn't find CKRecord for ramp. Defaulting to not ramped in");
140 operationError = [NSError errorWithDomain:octagonErrorDomain code:OTErrorRecordNotFound userInfo:@{NSLocalizedDescriptionKey: @" Couldn't find CKRecord for ramp. Defaulting to not ramped in"}];
142 recordRampStateFetchCompletionBlock(featureAllowed, featurePromoted, featureVisible, retryAfter, operationError);
145 [self.database addOperation: operation];
146 secnotice("octagon", "Attempting to fetch ramp state from CloudKit");
149 -(BOOL) checkRampState:(NSInteger*)retryAfter qos:(NSQualityOfService)qos error:(NSError**)error
151 __block BOOL isFeatureEnabled = NO;
152 __block NSError* localError = nil;
153 __block NSInteger localRetryAfter = 0;
155 if(self.lockStateTracker.isLocked){
156 secnotice("octagon","device is locked! can't check ramp state");
157 localError = [NSError errorWithDomain:(__bridge NSString*)kSecErrorDomain
158 code:errSecInteractionNotAllowed
159 userInfo:@{NSLocalizedDescriptionKey: @"device is locked"}];
165 if(self.accountTracker.currentCKAccountInfo.accountStatus != CKAccountStatusAvailable){
166 secnotice("octagon","not signed in! can't check ramp state");
167 localError = [NSError errorWithDomain:octagonErrorDomain
168 code:OTErrorNotSignedIn
169 userInfo:@{NSLocalizedDescriptionKey: @"not signed in"}];
175 if(!self.reachabilityTracker.currentReachability){
176 secnotice("octagon","no network! can't check ramp state");
177 localError = [NSError errorWithDomain:octagonErrorDomain
178 code:OTErrorNoNetwork
179 userInfo:@{NSLocalizedDescriptionKey: @"no network"}];
186 //defaults write to for whether or not a ramp record returns "enabled or disabled"
187 CFBooleanRef enabled = (CFBooleanRef)CFPreferencesCopyValue((__bridge CFStringRef)self.recordName,
188 CFSTR("com.apple.security"),
189 kCFPreferencesAnyUser, kCFPreferencesAnyHost);
190 if(enabled && CFGetTypeID(enabled) == CFBooleanGetTypeID()){
191 BOOL localConfigEnable = (enabled == kCFBooleanTrue);
192 secnotice("octagon", "feature is %@: %@ (local config)", localConfigEnable ? @"enabled" : @"disabled", self.recordName);
193 CFReleaseNull(enabled);
194 return localConfigEnable;
196 CFReleaseNull(enabled);
198 CKKSAnalytics* logger = [CKKSAnalytics logger];
199 SFAnalyticsActivityTracker *tracker = [logger logSystemMetricsForActivityNamed:CKKSActivityOTFetchRampState withAction:nil];
201 dispatch_semaphore_t sema = dispatch_semaphore_create(0);
205 [self fetchRampRecord:qos reply:^(BOOL featureAllowed, BOOL featurePromoted, BOOL featureVisible, NSInteger retryAfter, NSError *rampStateFetchError) {
206 secnotice("octagon", "fetch ramp records returned with featureAllowed: %d,\n featurePromoted: %d,\n featureVisible: %d,\n", featureAllowed, featurePromoted, featureVisible);
208 isFeatureEnabled = featureAllowed;
209 localRetryAfter = retryAfter;
210 if(rampStateFetchError){
211 localError = rampStateFetchError;
213 dispatch_semaphore_signal(sema);
216 long timeout = (SecCKKSTestsEnabled() ? 2*NSEC_PER_SEC : NSEC_PER_SEC * 65);
217 if(dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, timeout)) != 0) {
218 secnotice("octagon", "timed out waiting for response from CloudKit\n");
219 localError = [NSError errorWithDomain:octagonErrorDomain code:OTErrorCKTimeOut userInfo:@{NSLocalizedDescriptionKey: @"Failed to deserialize bottle peer"}];
221 [logger logUnrecoverableError:localError forEvent:OctagonEventRamp withAttributes:@{
222 OctagonEventAttributeFailureReason : @"cloud kit timed out"}
228 if(localRetryAfter > 0){
229 secnotice("octagon", "cloud kit asked security to retry: %ld", localRetryAfter);
230 *retryAfter = localRetryAfter;
234 secerror("octagon: had an error fetching ramp state: %@", localError);
235 [logger logUnrecoverableError:localError forEvent:OctagonEventRamp withAttributes:@{
236 OctagonEventAttributeFailureReason : @"fetching ramp state"}
242 if(isFeatureEnabled){
243 [logger logSuccessForEventNamed:OctagonEventRamp];
246 return isFeatureEnabled;
250 - (void)ckAccountStatusChange:(CKKSAccountStatus)oldStatus to:(CKKSAccountStatus)currentStatus {
251 secnotice("octagon", "%@ Received notification of CloudKit account status change, moving from %@ to %@",
252 self.zoneID.zoneName,
253 [CKKSCKAccountStateTracker stringFromAccountStatus: oldStatus],
254 [CKKSCKAccountStateTracker stringFromAccountStatus: currentStatus]);
256 switch(currentStatus) {
257 case CKKSAccountStatusAvailable: {
258 secnotice("octagon", "Logged into iCloud.");
259 self.accountStatus = CKKSAccountStatusAvailable;
263 case CKKSAccountStatusNoAccount: {
264 secnotice("octagon", "Logging out of iCloud. Shutting down.");
265 self.accountStatus = CKKSAccountStatusNoAccount;
269 case CKKSAccountStatusUnknown: {
270 // We really don't expect to receive this as a notification, but, okay!
271 secnotice("octagon", "Account status has become undetermined. Pausing for %@", self.zoneID.zoneName);
272 self.accountStatus = CKKSAccountStatusNoAccount;