]> git.saurik.com Git - apple/security.git/blob - keychain/ckks/CKKSCKAccountStateTracker.m
Security-58286.31.2.tar.gz
[apple/security.git] / keychain / ckks / CKKSCKAccountStateTracker.m
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 dispatch_sync(self.queue, ^{
173 self.firstSOSCircleFetch = true;
174
175 [self _onqueueUpdateAccountState:self.currentCKAccountInfo circle:circleStatus deliveredSemaphore:finishedSema];
176 });
177 return finishedSema;
178 }
179
180 // Takes the new ckAccountInfo we're moving to
181 -(void)_onqueueUpdateCKDeviceID: (CKAccountInfo*)ckAccountInfo {
182 dispatch_assert_queue(self.queue);
183 __weak __typeof(self) weakSelf = self;
184
185 // If we're in an account, opportunistically fill in the device id
186 if(ckAccountInfo.accountStatus == CKAccountStatusAvailable) {
187 [self.container fetchCurrentDeviceIDWithCompletionHandler:^(NSString* deviceID, NSError* ckerror) {
188 __strong __typeof(self) strongSelf = weakSelf;
189
190 // Make sure you synchronize here; if we've logged out before the callback returns, don't record the result
191 dispatch_async(strongSelf.queue, ^{
192 __strong __typeof(self) innerStrongSelf = weakSelf;
193 if(innerStrongSelf.currentCKAccountInfo.accountStatus == CKAccountStatusAvailable) {
194 secnotice("ckksaccount", "CloudKit deviceID is: %@ %@", deviceID, ckerror);
195
196 innerStrongSelf.ckdeviceID = deviceID;
197 innerStrongSelf.ckdeviceIDError = ckerror;
198 [innerStrongSelf.ckdeviceIDInitialized fulfill];
199 } else {
200 // Logged out! No ckdeviceid.
201 secerror("ckksaccount: Logged back out but still received a fetchCurrentDeviceIDWithCompletionHandler callback");
202
203 innerStrongSelf.ckdeviceID = nil;
204 innerStrongSelf.ckdeviceIDError = nil;
205 // Don't touch the ckdeviceIDInitialized object; it should have been reset when the logout happened.
206 }
207 });
208 }];
209 } else {
210 // Logging out? no more device ID.
211 self.ckdeviceID = nil;
212 self.ckdeviceIDError = nil;
213 self.ckdeviceIDInitialized = [[CKKSCondition alloc] init];
214 }
215 }
216
217 // Pulled out for mocking purposes
218 +(void)fetchCirclePeerID:(void (^)(NSString* _Nullable peerID, NSError* _Nullable error))callback {
219 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
220 CFErrorRef cferror = nil;
221 SOSPeerInfoRef egoPeerInfo = SOSCCCopyMyPeerInfo(&cferror);
222 NSString* egoPeerID = egoPeerInfo ? (NSString*)CFBridgingRelease(CFRetainSafe(SOSPeerInfoGetPeerID(egoPeerInfo))) : nil;
223 CFReleaseNull(egoPeerInfo);
224
225 callback(egoPeerID, CFBridgingRelease(cferror));
226 });
227 }
228
229 // Takes the new ckAccountInfo we're moving to
230 -(void)_onqueueUpdateCirclePeerID: (SOSCCStatus)sosccstatus {
231 dispatch_assert_queue(self.queue);
232 __weak __typeof(self) weakSelf = self;
233
234 // If we're in a circle, fetch the peer id
235 if(sosccstatus == kSOSCCInCircle) {
236 [CKKSCKAccountStateTracker fetchCirclePeerID:^(NSString* peerID, NSError* error) {
237 __strong __typeof(self) strongSelf = weakSelf;
238 dispatch_async(strongSelf.queue, ^{
239 __strong __typeof(self) innerstrongSelf = weakSelf;
240
241 if(innerstrongSelf.currentCircleStatus == kSOSCCInCircle) {
242 secnotice("ckksaccount", "Circle peerID is: %@ %@", peerID, error);
243 // Still in circle. Proceed.
244 innerstrongSelf.accountCirclePeerID = peerID;
245 innerstrongSelf.accountCirclePeerIDError = error;
246 [innerstrongSelf.accountCirclePeerIDInitialized fulfill];
247 } else {
248 secerror("ckksaccount: Out of circle but still received a fetchCirclePeerID callback");
249 // Not in-circle. Throw away circle id.
250 strongSelf.accountCirclePeerID = nil;
251 strongSelf.accountCirclePeerIDError = nil;
252 // Don't touch the accountCirclePeerIDInitialized object; it should have been reset when the logout happened.
253 }
254 });
255 }];
256 } else {
257 // Not in-circle, reset circle ID
258 secnotice("ckksaccount", "out of circle(%d): resetting peer ID", sosccstatus);
259 self.accountCirclePeerID = nil;
260 self.accountCirclePeerIDError = nil;
261 self.accountCirclePeerIDInitialized = [[CKKSCondition alloc] init];
262 }
263 }
264
265 -(void)_onqueueUpdateAccountState: (CKAccountInfo*) ckAccountInfo circle: (SOSCCStatus) sosccstatus deliveredSemaphore: (dispatch_semaphore_t) finishedSema {
266 dispatch_assert_queue(self.queue);
267
268 if([self.currentCKAccountInfo isEqual: ckAccountInfo] && self.currentCircleStatus == sosccstatus) {
269 // no-op.
270 secinfo("ckksaccount", "received another notification of CK Account State %@ and Circle status %d", ckAccountInfo, (int)sosccstatus);
271 dispatch_semaphore_signal(finishedSema);
272 return;
273 }
274
275 if(![self.currentCKAccountInfo isEqual: ckAccountInfo]) {
276 secnotice("ckksaccount", "moving to CK Account info: %@", ckAccountInfo);
277 self.currentCKAccountInfo = ckAccountInfo;
278
279 [self _onqueueUpdateCKDeviceID: ckAccountInfo];
280 }
281 if(self.currentCircleStatus != sosccstatus) {
282 secnotice("ckksaccount", "moving to circle status: %@", SOSCCGetStatusDescription(sosccstatus));
283 self.currentCircleStatus = sosccstatus;
284
285 [self _onqueueUpdateCirclePeerID: sosccstatus];
286 }
287
288 if(!self.firstSOSCircleFetch || !self.firstCKAccountFetch) {
289 secnotice("ckksaccount", "Haven't received updates from all sources; not passing update along: %@", self);
290 dispatch_semaphore_signal(finishedSema);
291 return;
292 }
293
294 // We are CKKSAccountStatusAvailable if we are:
295 // in CKAccountStatusAvailable
296 // and in circle
297 // and supportsDeviceToDeviceEncryption == true
298 CKKSAccountStatus oldComputedStatus = self.currentComputedAccountStatus;
299
300 if(self.currentCKAccountInfo) {
301 if(self.currentCKAccountInfo.accountStatus == CKAccountStatusAvailable) {
302 // CloudKit thinks we're logged in. Double check!
303 if(self.currentCKAccountInfo.supportsDeviceToDeviceEncryption && self.currentCircleStatus == kSOSCCInCircle) {
304 self.currentComputedAccountStatus = CKKSAccountStatusAvailable;
305 } else {
306 self.currentComputedAccountStatus = CKKSAccountStatusNoAccount;
307 }
308
309 } else {
310 // Account status is not CKAccountStatusAvailable; no more checking required.
311 self.currentComputedAccountStatus = CKKSAccountStatusNoAccount;
312 }
313 } else {
314 // No CKAccountInfo? We haven't received an update from cloudd yet; Change nothing.
315 }
316
317 if(oldComputedStatus == self.currentComputedAccountStatus) {
318 secnotice("ckksaccount", "No change in computed account status: %@ (%@ %@)",
319 [self currentStatus],
320 self.currentCKAccountInfo,
321 SOSCCGetStatusDescription(self.currentCircleStatus));
322 dispatch_semaphore_signal(finishedSema);
323 return;
324 }
325
326 secnotice("ckksaccount", "New computed account status: %@ (%@ %@)",
327 [self currentStatus],
328 self.currentCKAccountInfo,
329 SOSCCGetStatusDescription(self.currentCircleStatus));
330
331 dispatch_group_t g = dispatch_group_create();
332 if(!g) {
333 secnotice("ckksaccount", "Unable to get dispatch group.");
334 return;
335 }
336
337 NSEnumerator *enumerator = [self.changeListeners keyEnumerator];
338 dispatch_queue_t dq;
339
340 // Queue up the changes for each listener.
341 while ((dq = [enumerator nextObject])) {
342 id<CKKSAccountStateListener> listener = [self.changeListeners objectForKey: dq];
343 __weak __typeof(listener) weakListener = listener;
344
345 if(listener) {
346 dispatch_group_async(g, dq, ^{
347 [weakListener ckAccountStatusChange: oldComputedStatus to: self.currentComputedAccountStatus];
348 });
349 }
350 }
351
352 dispatch_group_notify(g, self.queue, ^{
353 dispatch_semaphore_signal(finishedSema);
354 });
355 }
356
357 -(void)notifyCKAccountStatusChangeAndWaitForSignal {
358 dispatch_semaphore_wait([self notifyCKAccountStatusChange: nil], DISPATCH_TIME_FOREVER);
359 }
360
361 -(void)notifyCircleStatusChangeAndWaitForSignal {
362 dispatch_semaphore_wait([self notifyCircleChange: nil], DISPATCH_TIME_FOREVER);
363 }
364
365 // This is its own function to allow OCMock to swoop in and replace the result during testing.
366 +(SOSCCStatus)getCircleStatus {
367 CFErrorRef cferror = NULL;
368
369 SOSCCStatus status = SOSCCThisDeviceIsInCircle(&cferror);
370 if(cferror) {
371 secerror("ckksaccount: error getting circle status: %@", cferror);
372 CFReleaseNull(cferror);
373 return kSOSCCError;
374 }
375 return status;
376 }
377
378 -(NSString*)currentStatus {
379 return [CKKSCKAccountStateTracker stringFromAccountStatus:self.currentComputedAccountStatus];
380 }
381
382 +(NSString*)stringFromAccountStatus: (CKKSAccountStatus) status {
383 switch(status) {
384 case CKKSAccountStatusUnknown: return @"account state unknown";
385 case CKKSAccountStatusAvailable: return @"logged in";
386 case CKKSAccountStatusNoAccount: return @"no account";
387 }
388 }
389
390 @end
391
392 #endif // OCTAGON