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/CKKSAPSReceiver.h"
27 #import "keychain/ckks/CKKSCondition.h"
28 #import <CloudKit/CloudKit_Private.h>
29 #include <utilities/debugging.h>
31 @implementation CKRecordZoneNotification (CKKSPushTracing)
32 - (void)setCkksPushTracingEnabled:(BOOL)ckksPushTracingEnabled {
33 objc_setAssociatedObject(self, "ckksPushTracingEnabled", ckksPushTracingEnabled ? @YES : @NO, OBJC_ASSOCIATION_RETAIN);
36 - (BOOL)ckksPushTracingEnabled {
37 return !![objc_getAssociatedObject(self, "ckksPushTracingEnabled") boolValue];
40 - (void)setCkksPushTracingUUID:(NSString*)ckksPushTracingUUID {
41 objc_setAssociatedObject(self, "ckksPushTracingUUID", ckksPushTracingUUID, OBJC_ASSOCIATION_RETAIN);
44 - (NSString*)ckksPushTracingUUID {
45 return objc_getAssociatedObject(self, "ckksPushTracingUUID");
48 - (void)setCkksPushReceivedDate:(NSDate*)ckksPushReceivedDate {
49 objc_setAssociatedObject(self, "ckksPushReceivedDate", ckksPushReceivedDate, OBJC_ASSOCIATION_RETAIN);
52 - (NSDate*)ckksPushReceivedDate {
53 return objc_getAssociatedObject(self, "ckksPushReceivedDate");
58 @interface CKKSAPSReceiver()
59 // If we receive notifications for a CKRecordZoneID that hasn't been registered yet, send them a their updates when they register
60 @property NSMutableDictionary<CKRecordZoneID*, NSMutableSet<CKRecordZoneNotification*>*>* undeliveredUpdates;
63 @implementation CKKSAPSReceiver
65 + (instancetype)receiverForEnvironment:(NSString *)environmentName
66 namedDelegatePort:(NSString*)namedDelegatePort
67 apsConnectionClass:(Class<CKKSAPSConnection>)apsConnectionClass {
68 static NSMutableDictionary<NSString*, CKKSAPSReceiver*>* environmentMap = nil;
70 if(environmentName == nil) {
71 secnotice("ckkspush", "No push environment; not bringing up APS.");
75 @synchronized([self class]) {
76 if(environmentMap == nil) {
77 environmentMap = [[NSMutableDictionary alloc] init];
80 CKKSAPSReceiver* recv = [environmentMap valueForKey: environmentName];
83 recv = [[CKKSAPSReceiver alloc] initWithEnvironmentName: environmentName namedDelegatePort:namedDelegatePort apsConnectionClass: apsConnectionClass];
84 [environmentMap setValue: recv forKey: environmentName];
91 + (dispatch_queue_t)apsDeliveryQueue {
92 static dispatch_queue_t aps_dispatch_queue;
93 static dispatch_once_t onceToken;
94 dispatch_once(&onceToken, ^{
95 aps_dispatch_queue = dispatch_queue_create("aps-callback-queue", DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL);
97 return aps_dispatch_queue;
100 - (instancetype)initWithEnvironmentName:(NSString*)environmentName
101 namedDelegatePort:(NSString*)namedDelegatePort
102 apsConnectionClass:(Class<CKKSAPSConnection>)apsConnectionClass {
103 if(self = [super init]) {
104 _apsConnectionClass = apsConnectionClass;
105 _apsConnection = NULL;
107 _undeliveredUpdates = [NSMutableDictionary dictionary];
109 // APS might be slow. This doesn't need to happen immediately, so let it happen later.
110 __weak __typeof(self) weakSelf = self;
111 dispatch_async([CKKSAPSReceiver apsDeliveryQueue], ^{
112 __strong __typeof(self) strongSelf = weakSelf;
116 strongSelf.apsConnection = [[strongSelf.apsConnectionClass alloc] initWithEnvironmentName:environmentName namedDelegatePort:namedDelegatePort queue:[CKKSAPSReceiver apsDeliveryQueue]];
117 strongSelf.apsConnection.delegate = strongSelf;
119 // The following string should match: [[NSBundle mainBundle] bundleIdentifier]
120 NSString* topic = [kCKPushTopicPrefix stringByAppendingString:@"com.apple.securityd"];
121 [strongSelf.apsConnection setEnabledTopics:@[topic]];
124 _zoneMap = [NSMapTable strongToWeakObjectsMapTable];
129 - (CKKSCondition*)registerReceiver:(id<CKKSZoneUpdateReceiver>)receiver forZoneID:(CKRecordZoneID *)zoneID {
130 CKKSCondition* finished = [[CKKSCondition alloc] init];
132 __weak __typeof(self) weakSelf = self;
133 dispatch_async([CKKSAPSReceiver apsDeliveryQueue], ^{
134 __strong __typeof(self) strongSelf = weakSelf;
136 secerror("ckks: received registration for released CKKSAPSReceiver");
140 [self.zoneMap setObject:receiver forKey: zoneID];
142 NSMutableSet<CKRecordZoneNotification*>* currentPendingMessages = self.undeliveredUpdates[zoneID];
143 [self.undeliveredUpdates removeObjectForKey:zoneID];
145 for(CKRecordZoneNotification* message in currentPendingMessages.allObjects) {
146 // Now, send the receiver its notification!
147 secerror("ckks: sending stored push(%@) to newly-registered zone(%@): %@", message, zoneID, receiver);
148 [receiver notifyZoneChange:message];
157 #pragma mark - APS Delegate callbacks
159 - (void)connection:(APSConnection *)connection didReceivePublicToken:(NSData *)publicToken {
161 secnotice("ckkspush", "CKKSAPSDelegate initiated: %@", connection);
164 - (void)connection:(APSConnection *)connection didReceiveToken:(NSData *)token forTopic:(NSString *)topic identifier:(NSString *)identifier {
165 secnotice("ckkspush", "Received per-topic push token \"%@\" for topic \"%@\" identifier \"%@\" on connection %@", token, topic, identifier, connection);
168 - (void)connection:(APSConnection *)connection didReceiveIncomingMessage:(APSIncomingMessage *)message {
169 secnotice("ckkspush", "CKKSAPSDelegate received a message: %@ on connection %@", message, connection);
171 // Report back through APS that we received a message
172 if(message.tracingEnabled) {
173 [connection confirmReceiptForMessage:message];
176 CKNotification* notification = [CKNotification notificationFromRemoteNotificationDictionary:message.userInfo];
178 if(notification.notificationType == CKNotificationTypeRecordZone) {
179 CKRecordZoneNotification* rznotification = (CKRecordZoneNotification*) notification;
180 rznotification.ckksPushTracingEnabled = message.tracingEnabled;
181 rznotification.ckksPushTracingUUID = message.tracingUUID ? [[[NSUUID alloc] initWithUUIDBytes:message.tracingUUID.bytes] UUIDString] : nil;
182 rznotification.ckksPushReceivedDate = [NSDate date];
184 // Find receiever in map
185 id<CKKSZoneUpdateReceiver> recv = [self.zoneMap objectForKey:rznotification.recordZoneID];
187 [recv notifyZoneChange:rznotification];
189 secerror("ckks: received push for unregistered zone: %@", rznotification);
190 if(rznotification.recordZoneID) {
191 NSMutableSet<CKRecordZoneNotification*>* currentPendingMessages = self.undeliveredUpdates[rznotification.recordZoneID];
192 if(currentPendingMessages) {
193 [currentPendingMessages addObject:rznotification];
195 self.undeliveredUpdates[rznotification.recordZoneID] = [NSMutableSet setWithObject:rznotification];
200 secerror("ckks: unexpected notification: %@", notification);