2 * Copyright (c) 2016 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 "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>
37 @implementation CKRecordZoneNotification (CKKSPushTracing)
38 - (void)setCkksPushTracingEnabled:(BOOL)ckksPushTracingEnabled {
39 objc_setAssociatedObject(self, "ckksPushTracingEnabled", ckksPushTracingEnabled ? @YES : @NO, OBJC_ASSOCIATION_RETAIN);
42 - (BOOL)ckksPushTracingEnabled {
43 return !![objc_getAssociatedObject(self, "ckksPushTracingEnabled") boolValue];
46 - (void)setCkksPushTracingUUID:(NSString*)ckksPushTracingUUID {
47 objc_setAssociatedObject(self, "ckksPushTracingUUID", ckksPushTracingUUID, OBJC_ASSOCIATION_RETAIN);
50 - (NSString*)ckksPushTracingUUID {
51 return objc_getAssociatedObject(self, "ckksPushTracingUUID");
54 - (void)setCkksPushReceivedDate:(NSDate*)ckksPushReceivedDate {
55 objc_setAssociatedObject(self, "ckksPushReceivedDate", ckksPushReceivedDate, OBJC_ASSOCIATION_RETAIN);
58 - (NSDate*)ckksPushReceivedDate {
59 return objc_getAssociatedObject(self, "ckksPushReceivedDate");
64 @interface OctagonAPSReceiver()
66 @property CKKSNearFutureScheduler *clearStalePushNotifications;
68 @property NSString* namedDelegatePort;
69 @property NSMutableDictionary<NSString*, id<OctagonAPSConnection>>* environmentMap;
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;
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;
78 @property (nullable) id<CKKSZoneUpdateReceiverProtocol> zoneUpdateReceiver;
79 @property NSMapTable<NSString*, id<OctagonCuttlefishUpdateReceiver>>* octagonContainerMap;
82 @implementation OctagonAPSReceiver
84 + (instancetype)receiverForNamedDelegatePort:(NSString*)namedDelegatePort
85 apsConnectionClass:(Class<OctagonAPSConnection>)apsConnectionClass
87 @synchronized([self class]) {
88 NSMutableDictionary<NSString*, OctagonAPSReceiver*>* delegatePortMap = [self synchronizedGlobalDelegatePortMap];
90 OctagonAPSReceiver* recv = delegatePortMap[namedDelegatePort];
93 recv = [[OctagonAPSReceiver alloc] initWithNamedDelegatePort:namedDelegatePort
94 apsConnectionClass:apsConnectionClass];
95 delegatePortMap[namedDelegatePort] = recv;
102 + (void)resetGlobalDelegatePortMap
104 @synchronized (self) {
105 [self resettableSynchronizedGlobalDelegatePortMap:YES];
109 + (NSMutableDictionary<NSString*, OctagonAPSReceiver*>*)synchronizedGlobalDelegatePortMap
111 return [self resettableSynchronizedGlobalDelegatePortMap:NO];
114 + (NSMutableDictionary<NSString*, OctagonAPSReceiver*>*)resettableSynchronizedGlobalDelegatePortMap:(BOOL)reset
116 static NSMutableDictionary<NSString*, OctagonAPSReceiver*>* delegatePortMap = nil;
118 if(delegatePortMap == nil || reset) {
119 delegatePortMap = [[NSMutableDictionary alloc] init];
122 return delegatePortMap;
125 - (NSArray<NSString *>*)registeredPushEnvironments
127 __block NSArray<NSString*>* environments = nil;
128 dispatch_sync([OctagonAPSReceiver apsDeliveryQueue], ^{
129 environments = [self.environmentMap allKeys];
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);
140 return aps_dispatch_queue;
143 - (BOOL) haveStalePushes
145 __block BOOL haveStalePushes = NO;
146 dispatch_sync([OctagonAPSReceiver apsDeliveryQueue], ^{
147 haveStalePushes = (self.undeliveredUpdates.count || self.undeliveredCuttlefishUpdates.count);
149 return haveStalePushes;
152 - (NSArray<NSString*>*)cuttlefishPushTopics
154 NSString* cuttlefishTopic = [kCKPushTopicPrefix stringByAppendingString:@"com.apple.security.cuttlefish"];
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"];
160 return @[cuttlefishTopic, tphTopic];
163 - (instancetype)initWithNamedDelegatePort:(NSString*)namedDelegatePort
164 apsConnectionClass:(Class<OctagonAPSConnection>)apsConnectionClass
166 return [self initWithNamedDelegatePort:namedDelegatePort
167 apsConnectionClass:apsConnectionClass
168 stalePushTimeout:5*60*NSEC_PER_SEC];
171 - (instancetype)initWithNamedDelegatePort:(NSString*)namedDelegatePort
172 apsConnectionClass:(Class<OctagonAPSConnection>)apsConnectionClass
173 stalePushTimeout:(uint64_t)stalePushTimeout
175 if((self = [super init])) {
176 _apsConnectionClass = apsConnectionClass;
178 _undeliveredUpdates = [NSMutableSet set];
179 _undeliveredCuttlefishUpdates = [[NSMutableSet alloc] init];
181 _namedDelegatePort = namedDelegatePort;
183 _environmentMap = [NSMutableDictionary dictionary];
185 _octagonContainerMap = [NSMapTable strongToWeakObjectsMapTable];
186 _zoneUpdateReceiver = nil;
189 void (^clearPushBlock)(void) = ^{
190 dispatch_async([OctagonAPSReceiver apsDeliveryQueue], ^{
191 NSMutableSet<CKRecordZoneNotification*> *droppedUpdates;
197 droppedUpdates = self.undeliveredUpdates;
199 self.undeliveredUpdates = [NSMutableSet set];
200 [self.undeliveredCuttlefishUpdates removeAllObjects];
202 [self reportDroppedPushes:droppedUpdates];
206 _clearStalePushNotifications = [[CKKSNearFutureScheduler alloc] initWithName: @"clearStalePushNotifications"
207 delay:stalePushTimeout
208 keepProcessAlive:false
209 dependencyDescriptionCode:CKKSResultDescriptionNone
210 block:clearPushBlock];
215 - (void)registerForEnvironment:(NSString*)environmentName
219 // APS might be slow. This doesn't need to happen immediately, so let it happen later.
220 dispatch_async([OctagonAPSReceiver apsDeliveryQueue], ^{
226 id<OctagonAPSConnection> apsConnection = self.environmentMap[environmentName];
228 // We've already set one of these up.
232 apsConnection = [[self.apsConnectionClass alloc] initWithEnvironmentName:environmentName namedDelegatePort:self.namedDelegatePort queue:[OctagonAPSReceiver apsDeliveryQueue]];
233 self.environmentMap[environmentName] = apsConnection;
235 apsConnection.delegate = self;
237 // The following string should match: [[NSBundle mainBundle] bundleIdentifier]
238 NSString* ckksTopic = [kCKPushTopicPrefix stringByAppendingString:@"com.apple.securityd"];
241 // Watches treat CKKS as opportunistic, and Octagon as normal priority.
242 apsConnection.enabledTopics = [self cuttlefishPushTopics];
243 apsConnection.opportunisticTopics = @[ckksTopic];
245 apsConnection.enabledTopics = [[self cuttlefishPushTopics] arrayByAddingObject:ckksTopic];
247 apsConnection.darkWakeTopics = self.apsConnection.enabledTopics;
248 #endif // TARGET_OS_OSX
250 #endif // TARGET_OS_WATCH
254 // Report that pushes we are dropping
255 - (void)reportDroppedPushes:(NSSet<CKRecordZoneNotification*>*)notifications
257 bool hasBeenUnlocked = false;
258 CFErrorRef error = NULL;
261 * Let server know that device is not unlocked yet
264 (void)SecAKSGetHasBeenUnlocked(&hasBeenUnlocked, &error);
265 CFReleaseNull(error);
267 NSString *eventName = @"CKKS APNS Push Dropped";
268 if (!hasBeenUnlocked) {
269 eventName = @"CKKS APNS Push Dropped - never unlocked";
272 for (CKRecordZoneNotification *notification in notifications) {
273 if (notification.ckksPushTracingEnabled) {
274 ckksnotice_global("apsnotification", "Submitting initial CKEventMetric due to notification %@", notification);
276 SecEventMetric *metric = [[SecEventMetric alloc] initWithEventName:@"APNSPushMetrics"];
277 metric[@"push_token_uuid"] = notification.ckksPushTracingUUID;
278 metric[@"push_received_date"] = notification.ckksPushReceivedDate;
280 metric[@"push_event_name"] = eventName;
282 [[SecMetrics managerObject] submitEvent:metric];
287 - (CKKSCondition*)registerCKKSReceiver:(id<CKKSZoneUpdateReceiverProtocol>)receiver
289 CKKSCondition* finished = [[CKKSCondition alloc] init];
292 dispatch_async([OctagonAPSReceiver apsDeliveryQueue], ^{
295 ckkserror_global("octagonpush", "received registration for released OctagonAPSReceiver");
299 ckksnotice_global("octagonpush", "Registering new CKKS push receiver: %@", receiver);
301 self.zoneUpdateReceiver = receiver;
303 NSMutableSet<CKRecordZoneNotification*>* currentPendingMessages = [self.undeliveredUpdates copy];
304 [self.undeliveredUpdates removeAllObjects];
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];
318 - (CKKSCondition*)registerCuttlefishReceiver:(id<OctagonCuttlefishUpdateReceiver>)receiver
319 forContainerName:(NSString*)containerName
321 CKKSCondition* finished = [[CKKSCondition alloc] init];
324 dispatch_async([OctagonAPSReceiver apsDeliveryQueue], ^{
327 ckkserror_global("octagonpush", "received registration for released OctagonAPSReceiver");
331 [self.octagonContainerMap setObject:receiver forKey:containerName];
332 if([self.undeliveredCuttlefishUpdates containsObject:containerName]) {
333 [self.undeliveredCuttlefishUpdates removeObject:containerName];
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];
346 #pragma mark - APS Delegate callbacks
348 - (void)connection:(APSConnection *)connection didReceivePublicToken:(NSData *)publicToken {
350 ckksnotice_global("octagonpush", "OctagonAPSDelegate initiated: %@", connection);
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);
357 - (void)connection:(APSConnection *)connection didReceiveIncomingMessage:(APSIncomingMessage *)message {
358 ckksnotice_global("octagonpush", "OctagonAPSDelegate received a message(%@): %@ ", message.topic, message.userInfo);
360 // Report back through APS that we received a message
361 if(message.tracingEnabled) {
362 [connection confirmReceiptForMessage:message];
365 // Separate and handle cuttlefish notifications
366 if(message.userInfo[@"cf"] != nil) {
367 NSDictionary* cfInfo = message.userInfo[@"cf"];
368 NSString* container = cfInfo[@"c"];
370 ckksnotice_global("octagonpush", "Received a cuttlefish push to container %@", container);
371 [[CKKSAnalytics logger] setDateProperty:[NSDate date] forKey:CKKSAnalyticsLastOctagonPush];
374 id<OctagonCuttlefishUpdateReceiver> receiver = [self.octagonContainerMap objectForKey:container];
377 [receiver notifyContainerChange:message];
379 ckkserror_global("octagonpush", "received cuttlefish push for unregistered container: %@", container);
380 [self.undeliveredCuttlefishUpdates addObject:container];
381 [self.clearStalePushNotifications trigger];
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];
395 CKNotification* notification = [CKNotification notificationFromRemoteNotificationDictionary:message.userInfo];
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];
403 [[CKKSAnalytics logger] setDateProperty:[NSDate date] forKey:CKKSAnalyticsLastCKKSPush];
405 // Find receiever in map
406 id<CKKSZoneUpdateReceiverProtocol> recv = self.zoneUpdateReceiver;
408 [recv notifyZoneChange:rznotification];
410 ckkserror_global("ckkspush", "received push for unregistered receiver: %@", rznotification);
411 [self.undeliveredUpdates addObject:rznotification];
412 [self.clearStalePushNotifications trigger];
415 ckkserror_global("ckkspush", "unexpected notification: %@", notification);