]> git.saurik.com Git - apple/security.git/blob - keychain/ot/OTRamping.m
Security-58286.51.6.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
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";
42
43 #define kCKRampManagerDefaultRetryTimeInSeconds 86400
44
45 #if OCTAGON
46 @interface OTRamp (lockstateTracker) <CKKSLockStateNotification>
47 @end
48 #endif
49
50 @interface OTRamp ()
51 @property (nonatomic, strong) CKContainer *container;
52 @property (nonatomic, strong) CKDatabase *database;
53
54 @property (nonatomic, strong) CKRecordZone *zone;
55 @property (nonatomic, strong) CKRecordZoneID *zoneID;
56
57 @property (nonatomic, strong) NSString *recordName;
58 @property (nonatomic, strong) NSString *featureName;
59 @property (nonatomic, strong) CKRecordID *recordID;
60
61 @property (nonatomic, strong) CKKSCKAccountStateTracker *accountTracker;
62 @property (nonatomic, strong) CKKSLockStateTracker *lockStateTracker;
63 @property (nonatomic, strong) CKKSReachabilityTracker *reachabilityTracker;
64
65 @property CKKSAccountStatus accountStatus;
66
67 @property (readonly) Class<CKKSFetchRecordsOperation> fetchRecordRecordsOperationClass;
68
69 @end
70
71 @implementation OTRamp
72
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
82
83 {
84 self = [super init];
85 if(self){
86 _container = container;
87 _recordName = [recordName copy];
88 _featureName = [featureName copy];
89 _database = database;
90 _zoneID = zoneID;
91 _accountTracker = accountTracker;
92 _lockStateTracker = lockStateTracker;
93 _reachabilityTracker = reachabilityTracker;
94 _fetchRecordRecordsOperationClass = fetchRecordRecordsOperationClass;
95 }
96 return self;
97 }
98
99 -(void) fetchRampRecord:(NSQualityOfService)qos reply:(void (^)(BOOL featureAllowed, BOOL featurePromoted, BOOL featureVisible, NSInteger retryAfter, NSError *rampStateFetchError))recordRampStateFetchCompletionBlock
100 {
101 __weak __typeof(self) weakSelf = self;
102
103 CKOperationConfiguration *opConfig = [[CKOperationConfiguration alloc] init];
104 opConfig.allowsCellularAccess = YES;
105 opConfig.qualityOfService = qos;
106
107 _recordID = [[CKRecordID alloc] initWithRecordName:_recordName zoneID:_zoneID];
108 CKFetchRecordsOperation *operation = [[[self.fetchRecordRecordsOperationClass class] alloc] initWithRecordIDs:@[ _recordID]];
109
110 operation.desiredKeys = @[kFeatureAllowedKey, kFeaturePromotedKey, kFeatureVisibleKey, kRetryAfterKey];
111
112 operation.configuration = opConfig;
113 operation.fetchRecordsCompletionBlock = ^(NSDictionary<CKRecordID *,CKRecord *> * _Nullable recordsByRecordID, NSError * _Nullable operationError) {
114 __strong __typeof(weakSelf) strongSelf = weakSelf;
115 if(!strongSelf) {
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);
119 return;
120 }
121
122 BOOL featureAllowed = NO;
123 BOOL featurePromoted = NO;
124 BOOL featureVisible = NO;
125 NSInteger retryAfter = kCKRampManagerDefaultRetryTimeInSeconds;
126
127 secnotice("octagon", "Fetch operation records %@ fetchError %@", recordsByRecordID, operationError);
128 // There should only be only one record.
129 CKRecord *rampRecord = recordsByRecordID[strongSelf.recordID];
130
131 if (rampRecord) {
132 featureAllowed = [rampRecord[kFeatureAllowedKey] boolValue];
133 featurePromoted = [rampRecord[kFeaturePromotedKey] boolValue];
134 featureVisible = [rampRecord[kFeatureVisibleKey] boolValue];
135 retryAfter = [rampRecord[kRetryAfterKey] integerValue];
136
137 secnotice("octagon", "Fetch ramp state - featureAllowed %@, featurePromoted: %@, featureVisible: %@, retryAfter: %ld", (featureAllowed ? @YES : @NO), (featurePromoted ? @YES : @NO), (featureVisible ? @YES : @NO), (long)retryAfter);
138 } else {
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"}];
141 }
142 recordRampStateFetchCompletionBlock(featureAllowed, featurePromoted, featureVisible, retryAfter, operationError);
143 };
144
145 [self.database addOperation: operation];
146 secnotice("octagon", "Attempting to fetch ramp state from CloudKit");
147 }
148
149 -(BOOL) checkRampState:(NSInteger*)retryAfter qos:(NSQualityOfService)qos error:(NSError**)error
150 {
151 __block BOOL isFeatureEnabled = NO;
152 __block NSError* localError = nil;
153 __block NSInteger localRetryAfter = 0;
154
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"}];
160 if(error){
161 *error = localError;
162 }
163 return NO;
164 }
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"}];
170 if(error){
171 *error = localError;
172 }
173 return NO;
174 }
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"}];
180 if(error){
181 *error = localError;
182 }
183 return NO;
184 }
185
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;
195 }
196 CFReleaseNull(enabled);
197
198 CKKSAnalytics* logger = [CKKSAnalytics logger];
199 SFAnalyticsActivityTracker *tracker = [logger logSystemMetricsForActivityNamed:CKKSActivityOTFetchRampState withAction:nil];
200
201 dispatch_semaphore_t sema = dispatch_semaphore_create(0);
202
203 [tracker start];
204
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);
207
208 isFeatureEnabled = featureAllowed;
209 localRetryAfter = retryAfter;
210 if(rampStateFetchError){
211 localError = rampStateFetchError;
212 }
213 dispatch_semaphore_signal(sema);
214 }];
215
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"}];
220
221 [logger logUnrecoverableError:localError forEvent:OctagonEventRamp withAttributes:@{
222 OctagonEventAttributeFailureReason : @"cloud kit timed out"}
223 ];
224 }
225
226 [tracker stop];
227
228 if(localRetryAfter > 0){
229 secnotice("octagon", "cloud kit asked security to retry: %ld", localRetryAfter);
230 *retryAfter = localRetryAfter;
231 }
232
233 if(localError){
234 secerror("octagon: had an error fetching ramp state: %@", localError);
235 [logger logUnrecoverableError:localError forEvent:OctagonEventRamp withAttributes:@{
236 OctagonEventAttributeFailureReason : @"fetching ramp state"}
237 ];
238 if(error){
239 *error = localError;
240 }
241 }
242 if(isFeatureEnabled){
243 [logger logSuccessForEventNamed:OctagonEventRamp];
244 }
245
246 return isFeatureEnabled;
247
248 }
249
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]);
255
256 switch(currentStatus) {
257 case CKKSAccountStatusAvailable: {
258 secnotice("octagon", "Logged into iCloud.");
259 self.accountStatus = CKKSAccountStatusAvailable;
260 }
261 break;
262
263 case CKKSAccountStatusNoAccount: {
264 secnotice("octagon", "Logging out of iCloud. Shutting down.");
265 self.accountStatus = CKKSAccountStatusNoAccount;
266 }
267 break;
268
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;
273
274 }
275 break;
276 }
277 }
278
279 @end
280 #endif
281
282