]> git.saurik.com Git - apple/security.git/blame - keychain/ckks/CKKSCKAccountStateTracker.m
Security-58286.1.32.tar.gz
[apple/security.git] / keychain / ckks / CKKSCKAccountStateTracker.m
CommitLineData
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