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