]>
Commit | Line | Data |
---|---|---|
866f8763 A |
1 | /* |
2 | * Copyright (c) 2017 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 | #include <dispatch/dispatch.h> | |
27 | #include <utilities/debugging.h> | |
28 | #include <Security/SecureObjectSync/SOSCloudCircle.h> | |
29 | #include <Security/SecureObjectSync/SOSCloudCircleInternal.h> | |
30 | #include <notify.h> | |
31 | ||
32 | #import "keychain/ckks/CKKS.h" | |
33 | #import "keychain/ckks/CKKSCKAccountStateTracker.h" | |
34 | ||
35 | ||
36 | @interface CKKSCKAccountStateTracker () | |
37 | @property (readonly) Class<CKKSNSNotificationCenter> nsnotificationCenterClass; | |
38 | ||
39 | @property CKKSAccountStatus currentComputedAccountStatus; | |
40 | ||
41 | @property dispatch_queue_t queue; | |
42 | ||
43 | @property NSMapTable<dispatch_queue_t, id<CKKSAccountStateListener>>* changeListeners; | |
44 | @property CKContainer* container; // used only for fetching the CKAccountStatus | |
45 | ||
46 | /* We have initialization races. We should report CKKSAccountStatusUnknown until both of | |
47 | * these are true, otherwise on a race, it looks like we logged out. */ | |
48 | @property bool firstCKAccountFetch; | |
49 | @property bool firstSOSCircleFetch; | |
50 | @end | |
51 | ||
52 | @implementation CKKSCKAccountStateTracker | |
53 | ||
54 | -(instancetype)init: (CKContainer*) container nsnotificationCenterClass: (Class<CKKSNSNotificationCenter>) nsnotificationCenterClass { | |
55 | if((self = [super init])) { | |
56 | _nsnotificationCenterClass = nsnotificationCenterClass; | |
57 | _changeListeners = [NSMapTable strongToWeakObjectsMapTable]; // Backwards from how we'd like, but it's the best way to have weak pointers to CKKSAccountStateListener. | |
58 | _currentCKAccountInfo = nil; | |
59 | _currentCircleStatus = kSOSCCError; | |
60 | ||
61 | _currentComputedAccountStatus = CKKSAccountStatusUnknown; | |
62 | ||
63 | _container = container; | |
64 | ||
65 | _queue = dispatch_queue_create("ck-account-state", DISPATCH_QUEUE_SERIAL); | |
66 | ||
67 | _firstCKAccountFetch = false; | |
68 | _firstSOSCircleFetch = false; | |
69 | ||
70 | _finishedInitialCalls = [[CKKSCondition alloc] init]; | |
71 | _ckdeviceIDInitialized = [[CKKSCondition alloc] init]; | |
72 | ||
73 | id<CKKSNSNotificationCenter> notificationCenter = [self.nsnotificationCenterClass defaultCenter]; | |
74 | secinfo("ckksaccount", "Registering with notification center %@", notificationCenter); | |
75 | [notificationCenter addObserver:self selector:@selector(notifyCKAccountStatusChange:) name:CKAccountChangedNotification object:NULL]; | |
76 | ||
77 | __weak __typeof(self) weakSelf = self; | |
78 | ||
79 | // If this is a live server, register with notify | |
80 | if(!SecCKKSTestsEnabled()) { | |
81 | int token = 0; | |
82 | notify_register_dispatch(kSOSCCCircleChangedNotification, &token, dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^(int t) { | |
83 | [weakSelf notifyCircleChange:nil]; | |
84 | }); | |
85 | } | |
86 | ||
87 | // Fire off a fetch of the account status. Do not go on our local queue, because notifyCKAccountStatusChange will attempt to go back on it for thread-safety. | |
88 | dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{ | |
89 | __strong __typeof(self) strongSelf = weakSelf; | |
90 | if(!strongSelf) { | |
91 | return; | |
92 | } | |
93 | [strongSelf notifyCKAccountStatusChange:nil]; | |
94 | [strongSelf notifyCircleChange:nil]; | |
95 | [strongSelf.finishedInitialCalls fulfill]; | |
96 | }); | |
97 | } | |
98 | return self; | |
99 | } | |
100 | ||
101 | -(void)dealloc { | |
102 | id<CKKSNSNotificationCenter> notificationCenter = [self.nsnotificationCenterClass defaultCenter]; | |
103 | [notificationCenter removeObserver:self]; | |
104 | } | |
105 | ||
106 | -(NSString*)descriptionInternal: (NSString*) selfString { | |
107 | return [NSString stringWithFormat:@"<%@: %@ (%@ %@)", | |
108 | selfString, | |
109 | [self currentStatus], | |
110 | self.currentCKAccountInfo, | |
111 | SOSCCGetStatusDescription(self.currentCircleStatus)]; | |
112 | } | |
113 | ||
114 | -(NSString*)description { | |
115 | return [self descriptionInternal: [[self class] description]]; | |
116 | } | |
117 | ||
118 | -(NSString*)debugDescription { | |
119 | return [self descriptionInternal: [super description]]; | |
120 | } | |
121 | ||
122 | -(CKKSAccountStatus)currentCKAccountStatusAndNotifyOnChange: (id<CKKSAccountStateListener>) listener { | |
123 | ||
124 | __block CKKSAccountStatus status = CKKSAccountStatusUnknown; | |
125 | ||
126 | dispatch_sync(self.queue, ^{ | |
127 | status = self.currentComputedAccountStatus; | |
128 | ||
129 | bool alreadyRegisteredListener = false; | |
130 | NSEnumerator *enumerator = [self.changeListeners objectEnumerator]; | |
131 | id<CKKSAccountStateListener> value; | |
132 | ||
133 | while ((value = [enumerator nextObject])) { | |
134 | // do pointer comparison | |
135 | alreadyRegisteredListener |= (value == listener); | |
136 | } | |
137 | ||
138 | if(listener && !alreadyRegisteredListener) { | |
139 | NSString* queueName = [NSString stringWithFormat: @"ck-account-state-%@", listener]; | |
140 | ||
141 | dispatch_queue_t objQueue = dispatch_queue_create([queueName UTF8String], DISPATCH_QUEUE_SERIAL); | |
142 | [self.changeListeners setObject: listener forKey: objQueue]; | |
143 | } | |
144 | }); | |
145 | return status; | |
146 | } | |
147 | ||
148 | - (dispatch_semaphore_t)notifyCKAccountStatusChange:(__unused id)object { | |
149 | // signals when this notify is Complete, including all downcalls. | |
150 | dispatch_semaphore_t finishedSema = dispatch_semaphore_create(0); | |
151 | ||
152 | [self.container accountInfoWithCompletionHandler:^(CKAccountInfo* ckAccountInfo, NSError * _Nullable error) { | |
153 | if(error) { | |
154 | secerror("ckksaccount: error getting account info: %@", error); | |
155 | dispatch_semaphore_signal(finishedSema); | |
156 | return; | |
157 | } | |
158 | ||
159 | dispatch_sync(self.queue, ^{ | |
160 | self.firstCKAccountFetch = true; | |
161 | [self _onqueueUpdateAccountState:ckAccountInfo circle:self.currentCircleStatus deliveredSemaphore:finishedSema]; | |
162 | }); | |
163 | }]; | |
164 | ||
165 | return finishedSema; | |
166 | } | |
167 | ||
168 | -(dispatch_semaphore_t)notifyCircleChange:(__unused id)object { | |
169 | dispatch_semaphore_t finishedSema = dispatch_semaphore_create(0); | |
170 | ||
171 | SOSCCStatus circleStatus = [CKKSCKAccountStateTracker getCircleStatus]; | |
172 | if(circleStatus == kSOSCCError) { | |
173 | dispatch_semaphore_signal(finishedSema); | |
174 | return finishedSema; | |
175 | } | |
176 | ||
177 | dispatch_sync(self.queue, ^{ | |
178 | self.firstSOSCircleFetch = true; | |
179 | ||
180 | [self _onqueueUpdateAccountState:self.currentCKAccountInfo circle:circleStatus deliveredSemaphore:finishedSema]; | |
181 | }); | |
182 | return finishedSema; | |
183 | } | |
184 | ||
185 | // Takes the new ckAccountInfo we're moving to | |
186 | -(void)_onqueueUpdateCKDeviceID: (CKAccountInfo*)ckAccountInfo { | |
187 | dispatch_assert_queue(self.queue); | |
188 | __weak __typeof(self) weakSelf = self; | |
189 | ||
190 | // If we're in an account, opportunistically fill in the device id | |
191 | if(ckAccountInfo.accountStatus == CKAccountStatusAvailable) { | |
192 | [self.container fetchCurrentDeviceIDWithCompletionHandler:^(NSString* deviceID, NSError* ckerror) { | |
193 | __strong __typeof(self) strongSelf = weakSelf; | |
194 | ||
195 | // Make sure you synchronize here; if we've logged out before the callback returns, don't record the result | |
196 | dispatch_async(strongSelf.queue, ^{ | |
197 | __strong __typeof(self) innerStrongSelf = weakSelf; | |
198 | if(innerStrongSelf.currentCKAccountInfo.accountStatus == CKAccountStatusAvailable) { | |
199 | secnotice("ckksaccount", "CloudKit deviceID is: %@ %@", deviceID, ckerror); | |
200 | ||
201 | innerStrongSelf.ckdeviceID = deviceID; | |
202 | innerStrongSelf.ckdeviceIDError = ckerror; | |
203 | [innerStrongSelf.ckdeviceIDInitialized fulfill]; | |
204 | } else { | |
205 | // Logged out! No ckdeviceid. | |
206 | secerror("ckksaccount: Logged back out but still received a fetchCurrentDeviceIDWithCompletionHandler callback"); | |
207 | ||
208 | innerStrongSelf.ckdeviceID = nil; | |
209 | innerStrongSelf.ckdeviceIDError = nil; | |
210 | // Don't touch the ckdeviceIDInitialized object; it should have been reset when the logout happened. | |
211 | } | |
212 | }); | |
213 | }]; | |
214 | } else { | |
215 | // Logging out? no more device ID. | |
216 | self.ckdeviceID = nil; | |
217 | self.ckdeviceIDError = nil; | |
218 | self.ckdeviceIDInitialized = [[CKKSCondition alloc] init]; | |
219 | } | |
220 | } | |
221 | ||
222 | // Pulled out for mocking purposes | |
223 | +(void)fetchCirclePeerID:(void (^)(NSString* _Nullable peerID, NSError* _Nullable error))callback { | |
224 | dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ | |
225 | CFErrorRef cferror = nil; | |
226 | SOSPeerInfoRef egoPeerInfo = SOSCCCopyMyPeerInfo(&cferror); | |
227 | NSString* egoPeerID = egoPeerInfo ? (NSString*)CFBridgingRelease(CFRetainSafe(SOSPeerInfoGetPeerID(egoPeerInfo))) : nil; | |
228 | CFReleaseNull(egoPeerInfo); | |
229 | ||
230 | callback(egoPeerID, CFBridgingRelease(cferror)); | |
231 | }); | |
232 | } | |
233 | ||
234 | // Takes the new ckAccountInfo we're moving to | |
235 | -(void)_onqueueUpdateCirclePeerID: (SOSCCStatus)sosccstatus { | |
236 | dispatch_assert_queue(self.queue); | |
237 | __weak __typeof(self) weakSelf = self; | |
238 | ||
239 | // If we're in a circle, fetch the peer id | |
240 | if(sosccstatus == kSOSCCInCircle) { | |
241 | [CKKSCKAccountStateTracker fetchCirclePeerID:^(NSString* peerID, NSError* error) { | |
242 | __strong __typeof(self) strongSelf = weakSelf; | |
243 | dispatch_async(strongSelf.queue, ^{ | |
244 | __strong __typeof(self) innerstrongSelf = weakSelf; | |
245 | ||
246 | if(innerstrongSelf.currentCircleStatus == kSOSCCInCircle) { | |
247 | secnotice("ckksaccount", "Circle peerID is: %@ %@", peerID, error); | |
248 | // Still in circle. Proceed. | |
249 | innerstrongSelf.accountCirclePeerID = peerID; | |
250 | innerstrongSelf.accountCirclePeerIDError = error; | |
251 | [innerstrongSelf.accountCirclePeerIDInitialized fulfill]; | |
252 | } else { | |
253 | secerror("ckksaccount: Out of circle but still received a fetchCirclePeerID callback"); | |
254 | // Not in-circle. Throw away circle id. | |
255 | strongSelf.accountCirclePeerID = nil; | |
256 | strongSelf.accountCirclePeerIDError = nil; | |
257 | // Don't touch the accountCirclePeerIDInitialized object; it should have been reset when the logout happened. | |
258 | } | |
259 | }); | |
260 | }]; | |
261 | } else { | |
262 | // Not in-circle, reset circle ID | |
263 | self.accountCirclePeerID = nil; | |
264 | self.accountCirclePeerIDError = nil; | |
265 | self.accountCirclePeerIDInitialized = [[CKKSCondition alloc] init]; | |
266 | } | |
267 | } | |
268 | ||
269 | -(void)_onqueueUpdateAccountState: (CKAccountInfo*) ckAccountInfo circle: (SOSCCStatus) sosccstatus deliveredSemaphore: (dispatch_semaphore_t) finishedSema { | |
270 | dispatch_assert_queue(self.queue); | |
271 | ||
272 | if([self.currentCKAccountInfo isEqual: ckAccountInfo] && self.currentCircleStatus == sosccstatus) { | |
273 | // no-op. | |
274 | secinfo("ckksaccount", "received another notification of CK Account State %@ and Circle status %d", ckAccountInfo, (int)sosccstatus); | |
275 | dispatch_semaphore_signal(finishedSema); | |
276 | return; | |
277 | } | |
278 | ||
279 | if(![self.currentCKAccountInfo isEqual: ckAccountInfo]) { | |
280 | secnotice("ckksaccount", "moving to CK Account info: %@", ckAccountInfo); | |
281 | self.currentCKAccountInfo = ckAccountInfo; | |
282 | ||
283 | [self _onqueueUpdateCKDeviceID: ckAccountInfo]; | |
284 | } | |
285 | if(self.currentCircleStatus != sosccstatus) { | |
286 | secnotice("ckksaccount", "moving to circle status: %@", SOSCCGetStatusDescription(sosccstatus)); | |
287 | self.currentCircleStatus = sosccstatus; | |
288 | ||
289 | [self _onqueueUpdateCirclePeerID: sosccstatus]; | |
290 | } | |
291 | ||
292 | if(!self.firstSOSCircleFetch || !self.firstCKAccountFetch) { | |
293 | secnotice("ckksaccount", "Haven't received updates from all sources; not passing update along: %@", self); | |
294 | dispatch_semaphore_signal(finishedSema); | |
295 | return; | |
296 | } | |
297 | ||
298 | // We are CKKSAccountStatusAvailable if we are: | |
299 | // in CKAccountStatusAvailable | |
300 | // and in circle | |
301 | // and supportsDeviceToDeviceEncryption == true | |
302 | CKKSAccountStatus oldComputedStatus = self.currentComputedAccountStatus; | |
303 | ||
304 | if(self.currentCKAccountInfo) { | |
305 | if(self.currentCKAccountInfo.accountStatus == CKAccountStatusAvailable) { | |
306 | // CloudKit thinks we're logged in. Double check! | |
307 | if(self.currentCKAccountInfo.supportsDeviceToDeviceEncryption && self.currentCircleStatus == kSOSCCInCircle) { | |
308 | self.currentComputedAccountStatus = CKKSAccountStatusAvailable; | |
309 | } else { | |
310 | self.currentComputedAccountStatus = CKKSAccountStatusNoAccount; | |
311 | } | |
312 | ||
313 | } else { | |
314 | // Account status is not CKAccountStatusAvailable; no more checking required. | |
315 | self.currentComputedAccountStatus = CKKSAccountStatusNoAccount; | |
316 | } | |
317 | } else { | |
318 | // No CKAccountInfo? We haven't received an update from cloudd yet; Change nothing. | |
319 | } | |
320 | ||
321 | if(oldComputedStatus == self.currentComputedAccountStatus) { | |
322 | secnotice("ckksaccount", "No change in computed account status: %@ (%@ %@)", | |
323 | [self currentStatus], | |
324 | self.currentCKAccountInfo, | |
325 | SOSCCGetStatusDescription(self.currentCircleStatus)); | |
326 | dispatch_semaphore_signal(finishedSema); | |
327 | return; | |
328 | } | |
329 | ||
330 | secnotice("ckksaccount", "New computed account status: %@ (%@ %@)", | |
331 | [self currentStatus], | |
332 | self.currentCKAccountInfo, | |
333 | SOSCCGetStatusDescription(self.currentCircleStatus)); | |
334 | ||
335 | dispatch_group_t g = dispatch_group_create(); | |
336 | if(!g) { | |
337 | secnotice("ckksaccount", "Unable to get dispatch group."); | |
338 | return; | |
339 | } | |
340 | ||
341 | NSEnumerator *enumerator = [self.changeListeners keyEnumerator]; | |
342 | dispatch_queue_t dq; | |
343 | ||
344 | // Queue up the changes for each listener. | |
345 | while ((dq = [enumerator nextObject])) { | |
346 | id<CKKSAccountStateListener> listener = [self.changeListeners objectForKey: dq]; | |
347 | __weak __typeof(listener) weakListener = listener; | |
348 | ||
349 | if(listener) { | |
350 | dispatch_group_async(g, dq, ^{ | |
351 | [weakListener ckAccountStatusChange: oldComputedStatus to: self.currentComputedAccountStatus]; | |
352 | }); | |
353 | } | |
354 | } | |
355 | ||
356 | dispatch_group_notify(g, self.queue, ^{ | |
357 | dispatch_semaphore_signal(finishedSema); | |
358 | }); | |
359 | } | |
360 | ||
361 | -(void)notifyCKAccountStatusChangeAndWaitForSignal { | |
362 | dispatch_semaphore_wait([self notifyCKAccountStatusChange: nil], DISPATCH_TIME_FOREVER); | |
363 | } | |
364 | ||
365 | -(void)notifyCircleStatusChangeAndWaitForSignal { | |
366 | dispatch_semaphore_wait([self notifyCircleChange: nil], DISPATCH_TIME_FOREVER); | |
367 | } | |
368 | ||
369 | // This is its own function to allow OCMock to swoop in and replace the result during testing. | |
370 | +(SOSCCStatus)getCircleStatus { | |
371 | CFErrorRef cferror = NULL; | |
372 | ||
373 | SOSCCStatus status = SOSCCThisDeviceIsInCircle(&cferror); | |
374 | if(cferror) { | |
375 | secerror("ckksaccount: error getting circle status: %@", cferror); | |
376 | CFReleaseNull(cferror); | |
377 | return kSOSCCError; | |
378 | } | |
379 | return status; | |
380 | } | |
381 | ||
382 | -(NSString*)currentStatus { | |
383 | return [CKKSCKAccountStateTracker stringFromAccountStatus:self.currentComputedAccountStatus]; | |
384 | } | |
385 | ||
386 | +(NSString*)stringFromAccountStatus: (CKKSAccountStatus) status { | |
387 | switch(status) { | |
388 | case CKKSAccountStatusUnknown: return @"account state unknown"; | |
389 | case CKKSAccountStatusAvailable: return @"logged in"; | |
390 | case CKKSAccountStatusNoAccount: return @"no account"; | |
391 | } | |
392 | } | |
393 | ||
394 | @end | |
395 | ||
396 | #endif // OCTAGON |