]> git.saurik.com Git - apple/security.git/blob - keychain/ckks/OctagonAPSReceiver.m
Security-59754.80.3.tar.gz
[apple/security.git] / keychain / ckks / OctagonAPSReceiver.m
1 /*
2 * Copyright (c) 2016 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 "keychain/ckks/OctagonAPSReceiver.h"
27 #import "keychain/ckks/CKKS.h"
28 #import "keychain/ckks/CKKSCondition.h"
29 #import "keychain/ckks/CKKSNearFutureScheduler.h"
30 #import "keychain/ckks/CKKSAnalytics.h"
31 #import "keychain/analytics/SecMetrics.h"
32 #import "keychain/analytics/SecEventMetric.h"
33 #import "keychain/ot/ObjCImprovements.h"
34 #import <CloudKit/CloudKit_Private.h>
35 #include <utilities/SecAKSWrappers.h>
36
37 @implementation CKRecordZoneNotification (CKKSPushTracing)
38 - (void)setCkksPushTracingEnabled:(BOOL)ckksPushTracingEnabled {
39 objc_setAssociatedObject(self, "ckksPushTracingEnabled", ckksPushTracingEnabled ? @YES : @NO, OBJC_ASSOCIATION_RETAIN);
40 }
41
42 - (BOOL)ckksPushTracingEnabled {
43 return !![objc_getAssociatedObject(self, "ckksPushTracingEnabled") boolValue];
44 }
45
46 - (void)setCkksPushTracingUUID:(NSString*)ckksPushTracingUUID {
47 objc_setAssociatedObject(self, "ckksPushTracingUUID", ckksPushTracingUUID, OBJC_ASSOCIATION_RETAIN);
48 }
49
50 - (NSString*)ckksPushTracingUUID {
51 return objc_getAssociatedObject(self, "ckksPushTracingUUID");
52 }
53
54 - (void)setCkksPushReceivedDate:(NSDate*)ckksPushReceivedDate {
55 objc_setAssociatedObject(self, "ckksPushReceivedDate", ckksPushReceivedDate, OBJC_ASSOCIATION_RETAIN);
56 }
57
58 - (NSDate*)ckksPushReceivedDate {
59 return objc_getAssociatedObject(self, "ckksPushReceivedDate");
60 }
61 @end
62
63
64 @interface OctagonAPSReceiver()
65
66 @property CKKSNearFutureScheduler *clearStalePushNotifications;
67
68 @property NSString* namedDelegatePort;
69 @property NSMutableDictionary<NSString*, id<OctagonAPSConnection>>* environmentMap;
70
71
72 // If we receive notifications for a record zone that hasn't been registered yet, send them a their updates when they register
73 @property NSMutableSet<CKRecordZoneNotification*>* undeliveredUpdates;
74
75 // Same, but for cuttlefish containers (and only remember that a push was received; don't remember the pushes themselves)
76 @property NSMutableSet<NSString*>* undeliveredCuttlefishUpdates;
77
78 @property (nullable) id<CKKSZoneUpdateReceiverProtocol> zoneUpdateReceiver;
79 @property NSMapTable<NSString*, id<OctagonCuttlefishUpdateReceiver>>* octagonContainerMap;
80 @end
81
82 @implementation OctagonAPSReceiver
83
84 + (instancetype)receiverForNamedDelegatePort:(NSString*)namedDelegatePort
85 apsConnectionClass:(Class<OctagonAPSConnection>)apsConnectionClass
86 {
87 @synchronized([self class]) {
88 NSMutableDictionary<NSString*, OctagonAPSReceiver*>* delegatePortMap = [self synchronizedGlobalDelegatePortMap];
89
90 OctagonAPSReceiver* recv = delegatePortMap[namedDelegatePort];
91
92 if(recv == nil) {
93 recv = [[OctagonAPSReceiver alloc] initWithNamedDelegatePort:namedDelegatePort
94 apsConnectionClass:apsConnectionClass];
95 delegatePortMap[namedDelegatePort] = recv;
96 }
97
98 return recv;
99 }
100 }
101
102 + (void)resetGlobalDelegatePortMap
103 {
104 @synchronized (self) {
105 [self resettableSynchronizedGlobalDelegatePortMap:YES];
106 }
107 }
108
109 + (NSMutableDictionary<NSString*, OctagonAPSReceiver*>*)synchronizedGlobalDelegatePortMap
110 {
111 return [self resettableSynchronizedGlobalDelegatePortMap:NO];
112 }
113
114 + (NSMutableDictionary<NSString*, OctagonAPSReceiver*>*)resettableSynchronizedGlobalDelegatePortMap:(BOOL)reset
115 {
116 static NSMutableDictionary<NSString*, OctagonAPSReceiver*>* delegatePortMap = nil;
117
118 if(delegatePortMap == nil || reset) {
119 delegatePortMap = [[NSMutableDictionary alloc] init];
120 }
121
122 return delegatePortMap;
123 }
124
125 - (NSArray<NSString *>*)registeredPushEnvironments
126 {
127 __block NSArray<NSString*>* environments = nil;
128 dispatch_sync([OctagonAPSReceiver apsDeliveryQueue], ^{
129 environments = [self.environmentMap allKeys];
130 });
131 return environments;
132 }
133
134 + (dispatch_queue_t)apsDeliveryQueue {
135 static dispatch_queue_t aps_dispatch_queue;
136 static dispatch_once_t onceToken;
137 dispatch_once(&onceToken, ^{
138 aps_dispatch_queue = dispatch_queue_create("aps-callback-queue", DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL);
139 });
140 return aps_dispatch_queue;
141 }
142
143 - (BOOL) haveStalePushes
144 {
145 __block BOOL haveStalePushes = NO;
146 dispatch_sync([OctagonAPSReceiver apsDeliveryQueue], ^{
147 haveStalePushes = (self.undeliveredUpdates.count || self.undeliveredCuttlefishUpdates.count);
148 });
149 return haveStalePushes;
150 }
151
152 - (NSArray<NSString*>*)cuttlefishPushTopics
153 {
154 NSString* cuttlefishTopic = [kCKPushTopicPrefix stringByAppendingString:@"com.apple.security.cuttlefish"];
155
156 // Currently cuttlefish pushes are sent to TPH. System XPC services can't properly register to be woken
157 // at push time, so receive them for it.
158 NSString* tphTopic = [kCKPushTopicPrefix stringByAppendingString:@"com.apple.TrustedPeersHelper"];
159
160 return @[cuttlefishTopic, tphTopic];
161 }
162
163 - (instancetype)initWithNamedDelegatePort:(NSString*)namedDelegatePort
164 apsConnectionClass:(Class<OctagonAPSConnection>)apsConnectionClass
165 {
166 return [self initWithNamedDelegatePort:namedDelegatePort
167 apsConnectionClass:apsConnectionClass
168 stalePushTimeout:5*60*NSEC_PER_SEC];
169 }
170
171 - (instancetype)initWithNamedDelegatePort:(NSString*)namedDelegatePort
172 apsConnectionClass:(Class<OctagonAPSConnection>)apsConnectionClass
173 stalePushTimeout:(uint64_t)stalePushTimeout
174 {
175 if((self = [super init])) {
176 _apsConnectionClass = apsConnectionClass;
177
178 _undeliveredUpdates = [NSMutableSet set];
179 _undeliveredCuttlefishUpdates = [[NSMutableSet alloc] init];
180
181 _namedDelegatePort = namedDelegatePort;
182
183 _environmentMap = [NSMutableDictionary dictionary];
184
185 _octagonContainerMap = [NSMapTable strongToWeakObjectsMapTable];
186 _zoneUpdateReceiver = nil;
187
188 WEAKIFY(self);
189 void (^clearPushBlock)(void) = ^{
190 dispatch_async([OctagonAPSReceiver apsDeliveryQueue], ^{
191 NSMutableSet<CKRecordZoneNotification*> *droppedUpdates;
192 STRONGIFY(self);
193 if (self == nil) {
194 return;
195 }
196
197 droppedUpdates = self.undeliveredUpdates;
198
199 self.undeliveredUpdates = [NSMutableSet set];
200 [self.undeliveredCuttlefishUpdates removeAllObjects];
201
202 [self reportDroppedPushes:droppedUpdates];
203 });
204 };
205
206 _clearStalePushNotifications = [[CKKSNearFutureScheduler alloc] initWithName: @"clearStalePushNotifications"
207 delay:stalePushTimeout
208 keepProcessAlive:false
209 dependencyDescriptionCode:CKKSResultDescriptionNone
210 block:clearPushBlock];
211 }
212 return self;
213 }
214
215 - (void)registerForEnvironment:(NSString*)environmentName
216 {
217 WEAKIFY(self);
218
219 // APS might be slow. This doesn't need to happen immediately, so let it happen later.
220 dispatch_async([OctagonAPSReceiver apsDeliveryQueue], ^{
221 STRONGIFY(self);
222 if(!self) {
223 return;
224 }
225
226 id<OctagonAPSConnection> apsConnection = self.environmentMap[environmentName];
227 if(apsConnection) {
228 // We've already set one of these up.
229 return;
230 }
231
232 apsConnection = [[self.apsConnectionClass alloc] initWithEnvironmentName:environmentName namedDelegatePort:self.namedDelegatePort queue:[OctagonAPSReceiver apsDeliveryQueue]];
233 self.environmentMap[environmentName] = apsConnection;
234
235 apsConnection.delegate = self;
236
237 // The following string should match: [[NSBundle mainBundle] bundleIdentifier]
238 NSString* ckksTopic = [kCKPushTopicPrefix stringByAppendingString:@"com.apple.securityd"];
239
240 #if TARGET_OS_WATCH
241 // Watches treat CKKS as opportunistic, and Octagon as normal priority.
242 apsConnection.enabledTopics = [self cuttlefishPushTopics];
243 apsConnection.opportunisticTopics = @[ckksTopic];
244 #else
245 apsConnection.enabledTopics = [[self cuttlefishPushTopics] arrayByAddingObject:ckksTopic];
246 #if TARGET_OS_OSX
247 apsConnection.darkWakeTopics = self.apsConnection.enabledTopics;
248 #endif // TARGET_OS_OSX
249
250 #endif // TARGET_OS_WATCH
251 });
252 }
253
254 // Report that pushes we are dropping
255 - (void)reportDroppedPushes:(NSSet<CKRecordZoneNotification*>*)notifications
256 {
257 bool hasBeenUnlocked = false;
258 CFErrorRef error = NULL;
259
260 /*
261 * Let server know that device is not unlocked yet
262 */
263
264 (void)SecAKSGetHasBeenUnlocked(&hasBeenUnlocked, &error);
265 CFReleaseNull(error);
266
267 NSString *eventName = @"CKKS APNS Push Dropped";
268 if (!hasBeenUnlocked) {
269 eventName = @"CKKS APNS Push Dropped - never unlocked";
270 }
271
272 for (CKRecordZoneNotification *notification in notifications) {
273 if (notification.ckksPushTracingEnabled) {
274 ckksnotice_global("apsnotification", "Submitting initial CKEventMetric due to notification %@", notification);
275
276 SecEventMetric *metric = [[SecEventMetric alloc] initWithEventName:@"APNSPushMetrics"];
277 metric[@"push_token_uuid"] = notification.ckksPushTracingUUID;
278 metric[@"push_received_date"] = notification.ckksPushReceivedDate;
279
280 metric[@"push_event_name"] = eventName;
281
282 [[SecMetrics managerObject] submitEvent:metric];
283 }
284 }
285 }
286
287 - (CKKSCondition*)registerCKKSReceiver:(id<CKKSZoneUpdateReceiverProtocol>)receiver
288 {
289 CKKSCondition* finished = [[CKKSCondition alloc] init];
290
291 WEAKIFY(self);
292 dispatch_async([OctagonAPSReceiver apsDeliveryQueue], ^{
293 STRONGIFY(self);
294 if(!self) {
295 ckkserror_global("octagonpush", "received registration for released OctagonAPSReceiver");
296 return;
297 }
298
299 ckksnotice_global("octagonpush", "Registering new CKKS push receiver: %@", receiver);
300
301 self.zoneUpdateReceiver = receiver;
302
303 NSMutableSet<CKRecordZoneNotification*>* currentPendingMessages = [self.undeliveredUpdates copy];
304 [self.undeliveredUpdates removeAllObjects];
305
306 for(CKRecordZoneNotification* message in currentPendingMessages.allObjects) {
307 // Now, send the receiver its notification!
308 ckkserror_global("octagonpush", "sending stored push(%@) to newly-registered receiver: %@", message, receiver);
309 [receiver notifyZoneChange:message];
310 }
311
312 [finished fulfill];
313 });
314
315 return finished;
316 }
317
318 - (CKKSCondition*)registerCuttlefishReceiver:(id<OctagonCuttlefishUpdateReceiver>)receiver
319 forContainerName:(NSString*)containerName
320 {
321 CKKSCondition* finished = [[CKKSCondition alloc] init];
322
323 WEAKIFY(self);
324 dispatch_async([OctagonAPSReceiver apsDeliveryQueue], ^{
325 STRONGIFY(self);
326 if(!self) {
327 ckkserror_global("octagonpush", "received registration for released OctagonAPSReceiver");
328 return;
329 }
330
331 [self.octagonContainerMap setObject:receiver forKey:containerName];
332 if([self.undeliveredCuttlefishUpdates containsObject:containerName]) {
333 [self.undeliveredCuttlefishUpdates removeObject:containerName];
334
335 // Now, send the receiver its fake notification!
336 ckkserror_global("octagonpush", "sending fake push to newly-registered cuttlefish receiver(%@): %@", containerName, receiver);
337 [receiver notifyContainerChange:nil];
338 }
339
340 [finished fulfill];
341 });
342
343 return finished;
344 }
345
346 #pragma mark - APS Delegate callbacks
347
348 - (void)connection:(APSConnection *)connection didReceivePublicToken:(NSData *)publicToken {
349 // no-op.
350 ckksnotice_global("octagonpush", "OctagonAPSDelegate initiated: %@", connection);
351 }
352
353 - (void)connection:(APSConnection *)connection didReceiveToken:(NSData *)token forTopic:(NSString *)topic identifier:(NSString *)identifier {
354 ckksnotice_global("octagonpush", "Received per-topic push token \"%@\" for topic \"%@\" identifier \"%@\" on connection %@", token, topic, identifier, connection);
355 }
356
357 - (void)connection:(APSConnection *)connection didReceiveIncomingMessage:(APSIncomingMessage *)message {
358 ckksnotice_global("octagonpush", "OctagonAPSDelegate received a message(%@): %@ ", message.topic, message.userInfo);
359
360 // Report back through APS that we received a message
361 if(message.tracingEnabled) {
362 [connection confirmReceiptForMessage:message];
363 }
364
365 // Separate and handle cuttlefish notifications
366 if(message.userInfo[@"cf"] != nil) {
367 NSDictionary* cfInfo = message.userInfo[@"cf"];
368 NSString* container = cfInfo[@"c"];
369
370 ckksnotice_global("octagonpush", "Received a cuttlefish push to container %@", container);
371 [[CKKSAnalytics logger] setDateProperty:[NSDate date] forKey:CKKSAnalyticsLastOctagonPush];
372
373 if(container) {
374 id<OctagonCuttlefishUpdateReceiver> receiver = [self.octagonContainerMap objectForKey:container];
375
376 if(receiver) {
377 [receiver notifyContainerChange:message];
378 } else {
379 ckkserror_global("octagonpush", "received cuttlefish push for unregistered container: %@", container);
380 [self.undeliveredCuttlefishUpdates addObject:container];
381 [self.clearStalePushNotifications trigger];
382 }
383 } else {
384 // APS stripped the container. Send a push to all registered containers.
385 @synchronized(self.octagonContainerMap) {
386 for(id<OctagonCuttlefishUpdateReceiver> receiver in [self.octagonContainerMap objectEnumerator]) {
387 [receiver notifyContainerChange:nil];
388 }
389 }
390 }
391
392 return;
393 }
394
395 CKNotification* notification = [CKNotification notificationFromRemoteNotificationDictionary:message.userInfo];
396
397 if(notification.notificationType == CKNotificationTypeRecordZone) {
398 CKRecordZoneNotification* rznotification = (CKRecordZoneNotification*) notification;
399 rznotification.ckksPushTracingEnabled = message.tracingEnabled;
400 rznotification.ckksPushTracingUUID = message.tracingUUID ? [[[NSUUID alloc] initWithUUIDBytes:message.tracingUUID.bytes] UUIDString] : nil;
401 rznotification.ckksPushReceivedDate = [NSDate date];
402
403 [[CKKSAnalytics logger] setDateProperty:[NSDate date] forKey:CKKSAnalyticsLastCKKSPush];
404
405 // Find receiever in map
406 id<CKKSZoneUpdateReceiverProtocol> recv = self.zoneUpdateReceiver;
407 if(recv) {
408 [recv notifyZoneChange:rznotification];
409 } else {
410 ckkserror_global("ckkspush", "received push for unregistered receiver: %@", rznotification);
411 [self.undeliveredUpdates addObject:rznotification];
412 [self.clearStalePushNotifications trigger];
413 }
414 } else {
415 ckkserror_global("ckkspush", "unexpected notification: %@", notification);
416 }
417 }
418
419 @end
420
421 #endif // OCTAGON