]> git.saurik.com Git - apple/security.git/blob - sec/SOSCircle/CloudKeychainProxy/CKDKVSProxy.m
Security-55471.14.8.tar.gz
[apple/security.git] / sec / SOSCircle / CloudKeychainProxy / CKDKVSProxy.m
1 /*
2 * Copyright (c) 2012-2013 Apple Computer, 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 //
25 // CKDKVSProxy.m
26 // ckd-xpc
27 //
28
29 #import <Foundation/NSUbiquitousKeyValueStore.h>
30 #import <Foundation/NSUbiquitousKeyValueStore_Private.h>
31 #import <Foundation/NSArray.h>
32 #import <Foundation/Foundation.h>
33
34 #import <Security/SecBasePriv.h>
35 #import <Security/SecItemPriv.h>
36 #import <utilities/debugging.h>
37 #import <notify.h>
38
39 #import "CKDKVSProxy.h"
40 #import "CKDPersistentState.h"
41 #import "CKDUserInteraction.h"
42
43 #import "SOSARCDefines.h"
44
45 #include <SecureObjectSync/SOSAccount.h>
46 #include <SecureObjectSync/SOSCloudCircleInternal.h>
47 #include "SOSCloudKeychainConstants.h"
48
49 #include <utilities/SecAKSWrappers.h>
50 #include <utilities/SecCFRelease.h>
51
52 #define SOSCKCSCOPE "sync"
53
54 /*
55 The total space available in your app’s iCloud key-value storage is 1 MB.
56 The maximum number of keys you can specify is 1024, and the size limit for
57 each value associated with a key is 1 MB. So, for example, if you store a
58 single large value of 1 MB for a single key, that consumes your total
59 available storage. If you store 1 KB of data for each key, you can use
60 1,000 key-value pairs.
61 */
62
63 static const char *kStreamName = "com.apple.notifyd.matching";
64
65
66 static NSString *kKeyAlwaysKeys = @"AlwaysKeys";
67 static NSString *kKeyFirstUnlockKeys = @"FirstUnlockKeys";
68 static NSString *kKeyUnlockedKeys = @"UnlockedKeys";
69 static NSString *kKeyPendingKeys = @"PendingKeys";
70 static NSString *kKeyUnsentChangedKeys = @"unsentChangedKeys";
71 static NSString *kKeyUnlockNotificationRequested = @"unlockNotificationRequested";
72 static NSString *kKeySyncWithPeersPending = @"SyncWithPeersPending";
73
74 enum
75 {
76 kCallbackMethodSecurityd = 0,
77 kCallbackMethodXPC = 1,
78 };
79
80 static const int64_t kMinSyncDelay = (NSEC_PER_MSEC * 500);
81 static const int64_t kMaxSyncDelay = (NSEC_PER_SEC * 5);
82 static const int64_t kMinSyncInterval = (NSEC_PER_SEC * 15);
83 static const int64_t kSyncTimerLeeway = (NSEC_PER_MSEC * 250);
84
85 @interface NSUbiquitousKeyValueStore (NSUbiquitousKeyValueStore_PrivateZ)
86 - (void) _synchronizeWithCompletionHandler:(void (^)(NSError *error))completionHandler;
87
88 /*
89 // SPI For Security
90 - (void) synchronizeWithCompletionHandler:(void (^)(NSError *error))completionHandler;
91 */
92
93 @end
94
95 @implementation UbiqitousKVSProxy
96
97
98 - (void)persistState
99 {
100 [SOSPersistentState setRegisteredKeys:[self exportKeyInterests]];
101 }
102
103 + (UbiqitousKVSProxy *) sharedKVSProxy
104 {
105 static UbiqitousKVSProxy *sharedKVSProxy;
106 if (!sharedKVSProxy) {
107 static dispatch_once_t onceToken;
108 dispatch_once(&onceToken, ^{
109 sharedKVSProxy = [[self alloc] init];
110 });
111 }
112 return sharedKVSProxy;
113 }
114
115 - (id)init
116 {
117 if (self = [super init])
118 {
119 secnotice("event", "%@ start", self);
120
121 _syncTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
122 dispatch_source_set_timer(_syncTimer, DISPATCH_TIME_FOREVER, DISPATCH_TIME_FOREVER, kSyncTimerLeeway);
123 dispatch_source_set_event_handler(_syncTimer, ^{
124 [self timerFired];
125 });
126 dispatch_resume(_syncTimer);
127
128 [[NSNotificationCenter defaultCenter]
129 addObserver: self
130 selector: @selector (iCloudAccountAvailabilityChanged:)
131 name: NSUbiquityIdentityDidChangeNotification
132 object: nil];
133
134 [[NSNotificationCenter defaultCenter] addObserver:self
135 selector:@selector(cloudChanged:)
136 name:NSUbiquitousKeyValueStoreDidChangeExternallyNotification
137 object:nil];
138
139 [self importKeyInterests: [SOSPersistentState registeredKeys]];
140
141 // Register for lock state changes
142 xpc_set_event_stream_handler(kStreamName, dispatch_get_main_queue(),
143 ^(xpc_object_t notification){
144 [self streamEvent:notification];
145 });
146
147 [self updateUnlockedSinceBoot];
148 [self updateIsLocked];
149 if (!_isLocked)
150 [self keybagDidUnlock];
151
152 secdebug(XPROXYSCOPE, "%@ done", self);
153 }
154 return self;
155 }
156
157 - (NSString *)description
158 {
159 return [NSString stringWithFormat:@"<%s%s%s%s%s%s%s%s%s>",
160 _isLocked ? "L" : "U",
161 _unlockedSinceBoot ? "B" : "-",
162 _seenKVSStoreChange ? "K" : "-",
163 _syncTimerScheduled ? "T" : "-",
164 _syncWithPeersPending ? "s" : "-",
165 [_pendingKeys count] ? "p" : "-",
166 _inCallout ? "C" : "-",
167 _shadowSyncWithPeersPending ? "S" : "-",
168 [_shadowPendingKeys count] ? "P" : "-"];
169 }
170
171 - (void)processAllItems
172 {
173 NSDictionary *allItems = [self getAll];
174 if (allItems)
175 {
176 secnotice("event", "%@ sending: %@", self, [[allItems allKeys] componentsJoinedByString: @" "]);
177 [self processKeyChangedEvent:allItems];
178 }
179 else
180 secdebug(XPROXYSCOPE, "%@ No items in KVS", self);
181 }
182
183 - (void)setItemsChangedBlock:(CloudItemsChangedBlock)itemsChangedBlock
184 {
185 self->itemsChangedCallback = itemsChangedBlock;
186 }
187
188 - (void)dealloc
189 {
190 secdebug(XPROXYSCOPE, "%@", self);
191 [[NSNotificationCenter defaultCenter] removeObserver:self
192 name:NSUbiquitousKeyValueStoreDidChangeExternallyNotification object:nil];
193
194 [[NSNotificationCenter defaultCenter] removeObserver:self
195 name:NSUbiquityIdentityDidChangeNotification object:nil];
196 }
197
198 // MARK: ----- Client Interface -----
199
200 - (void)setObject:(id)obj forKey:(id)key
201 {
202 secdebug("keytrace", "%@ key: %@, obj: %@", self, key, obj);
203 NSUbiquitousKeyValueStore *store = [self cloudStore];
204 if (store)
205 {
206 id value = [store objectForKey:key];
207 if (value)
208 secdebug("keytrace", "%@ Current value (before set) for key %@ is: %@", self, key, value);
209 else
210 secdebug("keytrace", "%@ No current value for key %@", self, key);
211 }
212 else
213 secdebug("keytrace", "Can't get store");
214
215 [[self cloudStore] setObject:obj forKey:key];
216 [self requestSynchronization:NO];
217 }
218
219 - (void)setObjectsFromDictionary:(NSDictionary *)values
220 {
221 secdebug(XPROXYSCOPE, "%@ start: %lu values to put", self, (unsigned long)[values count]);
222
223 NSUbiquitousKeyValueStore *store = [self cloudStore];
224 if (store && values)
225 {
226 [values enumerateKeysAndObjectsUsingBlock: ^(id key, id obj, BOOL *stop)
227 {
228 if (obj == NULL || obj == [NSNull null])
229 [store removeObjectForKey:key];
230 else
231 [store setObject:obj forKey:key];
232 }];
233
234 [self requestSynchronization:NO];
235 }
236 else
237 secdebug(XPROXYSCOPE, "%@ NULL? store: %@, values: %@", self, store, values);
238 }
239
240 - (void)requestSynchronization:(bool)force
241 {
242 if (force)
243 {
244 secdebug(XPROXYSCOPE, "%@ synchronize (forced)", self);
245 [[self cloudStore] synchronize];
246 }
247 else
248 {
249 secdebug(XPROXYSCOPE, "%@ synchronize (soon)", self);
250 [[self cloudStore] synchronize];
251 }
252 }
253
254 - (NSUbiquitousKeyValueStore *)cloudStore
255 {
256 NSUbiquitousKeyValueStore *iCloudStore = [NSUbiquitousKeyValueStore defaultStore];
257 // secdebug(XPROXYSCOPE, "cloudStore: %@", iCloudStore);
258 return iCloudStore;
259 }
260
261 // try to synchronize asap, and invoke the handler on completion to take incoming changes.
262 - (void)waitForSynchronization:(NSArray *)keys handler:(void (^)(NSDictionary *values, NSError *err))handler
263 {
264 secdebug(XPROXYSCOPE, "%@ synchronize and wait", self);
265
266 if (!keys)
267 {
268 NSError *err = [NSError errorWithDomain:(NSString *)NSPOSIXErrorDomain code:(NSInteger)ENOPROTOOPT userInfo:nil];
269 handler(@{}, err);
270 return;
271 }
272
273 secdebug(XPROXYSCOPE, "%@ Requesting synchronizeWithCompletionHandler", self);
274
275 [[self cloudStore] synchronizeWithCompletionHandler:^(NSError *error) {
276 [[self cloudStore] synchronize]; // Per olivier in <rdar://problem/13412631>, sync before getting values
277
278 NSDictionary * changedKeys = error ? @{} : [self copyChangedValues:[NSSet setWithArray:keys]];
279
280 secdebug(XPROXYSCOPE, "%@ Got synchronize completion %@ (error %@)", self, changedKeys, error);
281 handler(changedKeys, error);
282 }];
283 }
284
285 - (void)dumpStore
286 {
287 NSDictionary *dict = [[self cloudStore] dictionaryRepresentation];
288 [dict enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
289 secdebug(XPROXYSCOPE, "%@ Dump: value for key %@ is: %@", self, key, obj);
290 }];
291 }
292
293 - (void)removeObjectForKey:(NSString *)keyToRemove
294 {
295 [[self cloudStore] removeObjectForKey:keyToRemove];
296 }
297
298 - (void)clearStore
299 {
300 secdebug(XPROXYSCOPE, "%@ clearStore", self);
301 NSDictionary *dict = [[self cloudStore] dictionaryRepresentation];
302 NSArray *allKeys = [dict allKeys];
303 [allKeys enumerateObjectsUsingBlock:^(id key, NSUInteger idx, BOOL *stop)
304 {
305 secdebug(XPROXYSCOPE, "%@ Clearing value for key %@", self, key);
306 [[self cloudStore] removeObjectForKey:(NSString *)key];
307 }];
308
309 [self requestSynchronization:YES];
310 }
311
312
313 //
314 // MARK: ----- KVS key lists -----
315 //
316
317 - (id)get:(id)key
318 {
319 return [[self cloudStore] objectForKey:key];
320 }
321
322 - (NSDictionary *)getAll
323 {
324 return [[self cloudStore] dictionaryRepresentation];
325 }
326
327 - (NSDictionary*) exportKeyInterests
328 {
329 return @{ kKeyAlwaysKeys:[_alwaysKeys allObjects],
330 kKeyFirstUnlockKeys:[_firstUnlockKeys allObjects],
331 kKeyUnlockedKeys:[_unlockedKeys allObjects],
332 kKeyPendingKeys:[_pendingKeys allObjects],
333 kKeySyncWithPeersPending:[NSNumber numberWithBool:_syncWithPeersPending]
334 };
335 }
336
337 - (void) importKeyInterests: (NSDictionary*) interests
338 {
339 _alwaysKeys = [NSSet setWithArray: interests[kKeyAlwaysKeys]];
340 _firstUnlockKeys = [NSSet setWithArray: interests[kKeyFirstUnlockKeys]];
341 _unlockedKeys = [NSSet setWithArray: interests[kKeyUnlockedKeys]];
342 _pendingKeys = [NSMutableSet setWithArray: interests[kKeyPendingKeys]];
343 _syncWithPeersPending = [interests[kKeySyncWithPeersPending] boolValue];
344 }
345
346 - (NSMutableSet *)copyAllKeys
347 {
348 NSMutableSet *allKeys = [NSMutableSet setWithSet: _alwaysKeys];
349 [allKeys unionSet: _firstUnlockKeys];
350 [allKeys unionSet: _unlockedKeys];
351 return allKeys;
352 }
353
354 - (NSDictionary *)registerKeysAndGet:(BOOL)getNewKeysOnly always:(NSArray *)keysToRegister reqFirstUnlock:(NSArray *)reqFirstUnlock reqUnlocked:(NSArray *)reqUnlocked
355 {
356 NSMutableSet *allOldKeys = [self copyAllKeys];
357 _alwaysKeys = [NSSet setWithArray: keysToRegister];
358 _firstUnlockKeys = [NSSet setWithArray: reqFirstUnlock];
359 _unlockedKeys = [NSSet setWithArray: reqUnlocked];
360 NSMutableSet *allNewKeys = [self copyAllKeys];
361
362 [_pendingKeys intersectSet:allNewKeys];
363
364 NSSet* keysToGet = nil;
365 if (getNewKeysOnly) {
366 [allNewKeys minusSet:allOldKeys];
367 keysToGet = allNewKeys;
368 } else {
369 keysToGet = [self keysForCurrentLockState];
370 }
371
372 // Mark keysToGet for the current lockState as pending
373 NSSet *filteredKeys = [self filterKeySetWithLockState:keysToGet];
374 [self persistState];
375 NSMutableDictionary *returnedValues = [self copyChangedValues: filteredKeys];
376
377 secnotice("keytrace", "%@ [%s] always: %@ firstUnlock: %@ unlocked: %@ got: %@", self, getNewKeysOnly ? "new only " : "", [keysToRegister componentsJoinedByString: @" "], [reqFirstUnlock componentsJoinedByString: @" "], [reqUnlocked componentsJoinedByString: @" "], [[returnedValues allKeys] componentsJoinedByString: @" "]);
378
379 [self processKeyChangedEvent:returnedValues];
380 [returnedValues removeAllObjects];
381
382 return returnedValues;
383 }
384
385 - (NSDictionary *)localNotification:(NSDictionary *)localNotificationDict outFlags:(int64_t *)outFlags
386 {
387 __block bool done = false;
388 __block int64_t returnedFlags = 0;
389 __block NSDictionary *responses = NULL;
390 typedef bool (^CKDUserInteractionBlock) (CFDictionaryRef responses);
391
392 CKDUserInteraction *cui = [CKDUserInteraction sharedInstance];
393 [cui requestShowNotification:localNotificationDict completion:^ bool (CFDictionaryRef userResponses, int64_t flags)
394 {
395 responses = [NSDictionary dictionaryWithDictionary:(__bridge NSDictionary *)userResponses];
396 returnedFlags = flags;
397 secdebug(XPROXYSCOPE, "%@ requestShowNotification: dict: %@, flags: %#llx", self, responses, returnedFlags);
398 done = true;
399 return true;
400 }];
401
402 // TODO: replace with e.g. dispatch calls to wait, or semaphore
403 while (!done)
404 sleep(1);
405 if (outFlags)
406 {
407 secdebug(XPROXYSCOPE, "%@ outFlags: %#llx", self, returnedFlags);
408 *outFlags = returnedFlags;
409 }
410 return responses;
411 }
412
413 - (void)setParams:(NSDictionary *)paramsDict
414 {
415 secdebug(XPROXYSCOPE, "%@ setParams: %@ ", self, paramsDict);
416 NSString *cbmethod = [paramsDict objectForKey:(__bridge id)kParamCallbackMethod];
417 if (!cbmethod)
418 return;
419
420 if ([cbmethod isEqualToString:(__bridge NSString *)kParamCallbackMethodSecurityd])
421 callbackMethod = kCallbackMethodSecurityd;
422 else if ([cbmethod isEqualToString:(__bridge NSString *)kParamCallbackMethodXPC])
423 callbackMethod = kCallbackMethodXPC;
424 }
425
426 - (void)saveToUbiquitousStore
427 {
428 [self requestSynchronization:NO];
429 }
430
431 // MARK: ----- Event Handling -----
432
433 - (void)streamEvent:(xpc_object_t)notification
434 {
435 #if (!TARGET_IPHONE_SIMULATOR)
436 const char *notificationName = xpc_dictionary_get_string(notification, "Notification");
437 if (!notificationName) {
438 } else if (strcmp(notificationName, kUserKeybagStateChangeNotification)==0) {
439 return [self keybagStateChange];
440 } else if (strcmp(notificationName, kCloudKeychainStorechangeChangeNotification)==0) {
441 return [self kvsStoreChange];
442 } else if (strcmp(notificationName, kNotifyTokenForceUpdate)==0) {
443 // DEBUG -- Possibly remove in future
444 return [self processAllItems];
445 }
446 const char *eventName = xpc_dictionary_get_string(notification, "XPCEventName");
447 char *desc = xpc_copy_description(notification);
448 secerror("%@ event: %s name: %s desc: %s", self, eventName, notificationName, desc);
449 if (desc)
450 free((void *)desc);
451 #endif
452 }
453
454 - (void)iCloudAccountAvailabilityChanged:(NSNotification*)notification
455 {
456 /*
457 Continuing this example, you’d then implement an iCloudAccountAvailabilityChanged: method that would:
458
459 Call the ubiquityIdentityToken method and store its return value.
460 Compare the new value to the previous value, to find out if the user logged out of their account or
461 logged in to a different account. If the previously-used account is now unavailable, save the current
462 state locally as needed, empty your iCloud-related data caches, and refresh all iCloud-related user interface elements.
463 */
464 id previCloudToken = currentiCloudToken;
465 currentiCloudToken = [[NSFileManager defaultManager] ubiquityIdentityToken];
466 if (previCloudToken != currentiCloudToken)
467 secnotice("event", "%@ iCloud account changed!", self);
468 else
469 secnotice("event", "%@ %@", self, notification);
470 }
471
472 - (void)cloudChanged:(NSNotification*)notification
473 {
474 /*
475 Posted when the value of one or more keys in the local key-value store
476 changed due to incoming data pushed from iCloud. This notification is
477 sent only upon a change received from iCloud; it is not sent when your
478 app sets a value.
479
480 The user info dictionary can contain the reason for the notification as
481 well as a list of which values changed, as follows:
482
483 The value of the NSUbiquitousKeyValueStoreChangeReasonKey key, when
484 present, indicates why the key-value store changed. Its value is one of
485 the constants in "Change Reason Values."
486
487 The value of the NSUbiquitousKeyValueStoreChangedKeysKey, when present,
488 is an array of strings, each the name of a key whose value changed. The
489 notification object is the NSUbiquitousKeyValueStore object whose contents
490 changed.
491
492 NSUbiquitousKeyValueStoreInitialSyncChange is only posted if there is any
493 local value that has been overwritten by a distant value. If there is no
494 conflict between the local and the distant values when doing the initial
495 sync (e.g. if the cloud has no data stored or the client has not stored
496 any data yet), you'll never see that notification.
497
498 NSUbiquitousKeyValueStoreInitialSyncChange implies an initial round trip
499 with server but initial round trip with server does not imply
500 NSUbiquitousKeyValueStoreInitialSyncChange.
501 */
502
503 secdebug(XPROXYSCOPE, "%@ cloudChanged notification: %@", self, notification);
504
505 NSDictionary *userInfo = [notification userInfo];
506 NSNumber *reason = [userInfo objectForKey:NSUbiquitousKeyValueStoreChangeReasonKey];
507 if (reason) switch ([reason integerValue]) {
508 case NSUbiquitousKeyValueStoreInitialSyncChange:
509 case NSUbiquitousKeyValueStoreServerChange:
510 {
511 _seenKVSStoreChange = YES;
512 NSSet *keysChangedInCloud = [NSSet setWithArray:[userInfo objectForKey:NSUbiquitousKeyValueStoreChangedKeysKey]];
513 NSSet *keysOfInterestThatChanged = [self filterKeySetWithLockState:keysChangedInCloud];
514 NSMutableDictionary *changedValues = [self copyChangedValues:keysOfInterestThatChanged];
515 if ([reason integerValue] == NSUbiquitousKeyValueStoreInitialSyncChange)
516 changedValues[(__bridge NSString*)kSOSKVSInitialSyncKey] = @"true";
517
518 secnotice("event", "%@ keysChangedInCloud: %@ keysOfInterest: %@", self, [[keysChangedInCloud allObjects] componentsJoinedByString: @" "], [[changedValues allKeys] componentsJoinedByString: @" "]);
519 if ([changedValues count])
520 [self processKeyChangedEvent:changedValues];
521 break;
522 }
523 case NSUbiquitousKeyValueStoreQuotaViolationChange:
524 seccritical("%@ event received NSUbiquitousKeyValueStoreQuotaViolationChange", self);
525 break;
526 case NSUbiquitousKeyValueStoreAccountChange:
527 // The primary account changed. We do not get this for password changes on the same account
528 secnotice("event", "%@ NSUbiquitousKeyValueStoreAccountChange", self);
529 if ([reason integerValue] == NSUbiquitousKeyValueStoreInitialSyncChange)
530 {
531 NSDictionary *changedValues = @{ (__bridge NSString*)kSOSKVSAccountChangedKey: @"true" };
532 [self processKeyChangedEvent:changedValues];
533 }
534 break;
535 }
536 }
537
538 - (void) calloutWith: (void(^)(NSSet *pending, bool syncWithPeersPending, dispatch_queue_t queue, void(^done)(NSSet *handledKeys, bool handledSyncWithPeers))) callout
539 {
540 // In CKDKVSProxy's serial queue
541 NSSet *myPending = [_pendingKeys copy];
542 bool mySyncWithPeersPending = _syncWithPeersPending;
543 bool wasLocked = _isLocked;
544 _inCallout = YES;
545 _shadowPendingKeys = [NSMutableSet set];
546 _shadowSyncWithPeersPending = NO;
547 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
548 callout(myPending, mySyncWithPeersPending, dispatch_get_main_queue(), ^(NSSet *handledKeys, bool handledSyncWithPeers) {
549 // In CKDKVSProxy's serial queue
550 _inCallout = NO;
551
552 // Update SyncWithPeers stuff.
553 _syncWithPeersPending = (mySyncWithPeersPending && (!handledSyncWithPeers)) || _shadowSyncWithPeersPending;
554 _shadowSyncWithPeersPending = NO;
555 if (handledSyncWithPeers)
556 _lastSyncTime = dispatch_time(DISPATCH_TIME_NOW, 0);
557
558 // Update pendingKeys and handle them
559 [_pendingKeys minusSet: handledKeys];
560 bool hadShadowPendingKeys = [_shadowPendingKeys count];
561 NSSet *filteredKeys = [self filterKeySetWithLockState:_shadowPendingKeys];
562 _shadowPendingKeys = nil;
563
564 // Write state to disk
565 [self persistState];
566
567 // Handle shadow pended stuff
568 if (_syncWithPeersPending && !_isLocked)
569 [self scheduleSyncRequestTimer];
570 /* We don't want to call processKeyChangedEvent if we failed to
571 handle pending keys and the device didn't unlock nor receive
572 any kvs changes while we were in our callout.
573 Doing so will lead to securityd and CloudKeychainProxy
574 talking to each other forever in a tight loop if securityd
575 repeatedly returns an error processing the same message.
576 Instead we leave any old pending keys until the next event. */
577 if (hadShadowPendingKeys || (!_isLocked && wasLocked))
578 [self processKeyChangedEvent:[self copyChangedValues:filteredKeys]];
579 });
580 });
581 }
582
583 - (void) doSyncWithAllPeers
584 {
585 [self calloutWith:^(NSSet *pending, bool syncWithPeersPending, dispatch_queue_t queue, void(^done)(NSSet *, bool)) {
586 CFErrorRef error = NULL;
587 SyncWithAllPeersReason reason = SOSCCProcessSyncWithAllPeers(&error);
588 dispatch_async(queue, ^{
589 bool handledSyncWithPeers = NO;
590 if (reason == kSyncWithAllPeersSuccess) {
591 handledSyncWithPeers = YES;
592 secnotice("event", "%@ syncWithAllPeers succeeded", self);
593 } else if (reason == kSyncWithAllPeersLocked) {
594 secnotice("event", "%@ syncWithAllPeers attempted while locked - waiting for unlock", self);
595 handledSyncWithPeers = YES;
596 } else if (reason == kSyncWithAllPeersOtherFail) {
597 // Pretend we handled syncWithPeers, by pushing out the _lastSyncTime
598 // This will cause us to wait for kMinSyncInterval seconds before
599 // retrying, so we don't spam securityd if sync is failing
600 secerror("%@ syncWithAllPeers %@, rescheduling timer", self, error);
601 _lastSyncTime = dispatch_time(DISPATCH_TIME_NOW, 0);
602 } else {
603 secerror("%@ syncWithAllPeers %@, unknown reason: %d", self, error, reason);
604 }
605
606 done(nil, handledSyncWithPeers);
607 CFReleaseSafe(error);
608 });
609 }];
610 }
611
612 - (void)timerFired
613 {
614 secnotice("event", "%@ syncWithPeersPending: %d inCallout: %d isLocked: %d", self, _syncWithPeersPending, _inCallout, _isLocked);
615 _syncTimerScheduled = NO;
616 if (_syncWithPeersPending && !_inCallout && !_isLocked)
617 [self doSyncWithAllPeers];
618 }
619
620 - (dispatch_time_t) nextSyncTime
621 {
622 dispatch_time_t nextSync = dispatch_time(DISPATCH_TIME_NOW, kMinSyncDelay);
623
624 // Don't sync again unless we waited at least kMinSyncInterval
625 if (_lastSyncTime) {
626 dispatch_time_t soonest = dispatch_time(_lastSyncTime, kMinSyncInterval);
627 if (nextSync < soonest || _deadline < soonest) {
628 secdebug("timer", "%@ backing off", self);
629 return soonest;
630 }
631 }
632
633 // Don't delay more than kMaxSyncDelay after the first request.
634 if (nextSync > _deadline) {
635 secdebug("timer", "%@ hit deadline", self);
636 return _deadline;
637 }
638
639 // Bump the timer by kMinSyncDelay
640 if (_syncTimerScheduled)
641 secdebug("timer", "%@ bumped timer", self);
642 else
643 secdebug("timer", "%@ scheduled timer", self);
644
645 return nextSync;
646 }
647
648 - (void)scheduleSyncRequestTimer
649 {
650 dispatch_source_set_timer(_syncTimer, [self nextSyncTime], DISPATCH_TIME_FOREVER, kSyncTimerLeeway);
651 _syncTimerScheduled = YES;
652 }
653
654 - (void)requestSyncWithAllPeers // secd calling SOSCCSyncWithAllPeers invokes this
655 {
656 secdebug("event", "%@ _syncWithPeersPending: %d _inCallout: %d _shadowSyncWithPeersPending: %d _isLocked: %d", self, _syncWithPeersPending, _inCallout, _shadowSyncWithPeersPending, _isLocked);
657
658 if (!_syncWithPeersPending || (_inCallout && !_shadowSyncWithPeersPending))
659 _deadline = dispatch_time(DISPATCH_TIME_NOW, kMaxSyncDelay);
660
661 if (!_syncWithPeersPending) {
662 _syncWithPeersPending = YES;
663 [self persistState];
664 }
665
666 if (_inCallout)
667 _shadowSyncWithPeersPending = YES;
668
669 if (!_isLocked)
670 [self scheduleSyncRequestTimer];
671 }
672
673 - (BOOL) updateUnlockedSinceBoot
674 {
675 CFErrorRef aksError = NULL;
676 if (!SecAKSGetHasBeenUnlocked(&_unlockedSinceBoot, &aksError)) {
677 secerror("%@ Got error from SecAKSGetHasBeenUnlocked: %@", self, aksError);
678 CFReleaseSafe(aksError);
679 return NO;
680 }
681 return YES;
682 }
683
684 - (BOOL) updateIsLocked
685 {
686 CFErrorRef aksError = NULL;
687 if (!SecAKSGetIsLocked(&_isLocked, &aksError)) {
688 secerror("%@ Got error querying lock state: %@", self, aksError);
689 CFReleaseSafe(aksError);
690 return NO;
691 }
692 if (!_isLocked)
693 _unlockedSinceBoot = YES;
694 return YES;
695 }
696
697 - (void) keybagStateChange
698 {
699 BOOL wasLocked = _isLocked;
700 if ([self updateIsLocked]) {
701 if (wasLocked == _isLocked)
702 secdebug("event", "%@ still %s ignoring", self, _isLocked ? "locked" : "unlocked");
703 else if (_isLocked)
704 [self keybagDidLock];
705 else
706 [self keybagDidUnlock];
707 }
708 }
709
710 - (void) keybagDidLock
711 {
712 secnotice("event", "%@", self);
713 }
714
715 - (void) keybagDidUnlock
716 {
717 secnotice("event", "%@", self);
718 // First send changed keys to securityd so it can proccess updates
719 NSSet *filteredKeys = [self filterKeySetWithLockState:nil];
720 [self processKeyChangedEvent:[self copyChangedValues:filteredKeys]];
721
722 // Then, tickle securityd to perform a sync if needed.
723 if (_syncWithPeersPending && !_syncTimerScheduled) {
724 [self doSyncWithAllPeers];
725 }
726 }
727
728 - (void) kvsStoreChange {
729 if (!_seenKVSStoreChange) {
730 _seenKVSStoreChange = YES; // Only do this once
731 secnotice("event", "%@ received darwin notification before first NSNotification", self);
732 // TODO This might not be needed if we always get the NSNotification
733 // deleived even if we were launched due to a kvsStoreChange
734 // Send all keys for current lock state to securityd so it can proccess them
735 NSSet *filteredKeys = [self filterKeySetWithLockState:[self copyAllKeys]];
736 [self processKeyChangedEvent:[self copyChangedValues:filteredKeys]];
737 } else {
738 secdebug("event", "%@ ignored, waiting for NSNotification", self);
739 }
740 }
741
742 //
743 // MARK: ----- Key Filtering -----
744 //
745
746 - (NSSet*) keysForCurrentLockState
747 {
748 secdebug("keytrace", "%@ Filtering: unlockedSinceBoot: %d\n unlocked: %d\n, _keysOfInterest: %@", self, (int) _unlockedSinceBoot, (int) !_isLocked, [self exportKeyInterests]);
749
750 NSMutableSet *currentStateKeys = [NSMutableSet setWithSet: _alwaysKeys];
751 if (_unlockedSinceBoot)
752 [currentStateKeys unionSet: _firstUnlockKeys];
753
754 if (!_isLocked)
755 [currentStateKeys unionSet: _unlockedKeys];
756
757 return currentStateKeys;
758 }
759
760 - (NSSet*) filterKeySetWithLockState: (NSSet*) startingSet
761 {
762 NSSet* availableKeys = [self keysForCurrentLockState];
763 NSSet *allKeys = [self copyAllKeys];
764 if (_inCallout) {
765 [_shadowPendingKeys unionSet:startingSet];
766 [_shadowPendingKeys intersectSet:allKeys];
767 }
768 [_pendingKeys unionSet:startingSet];
769 [_pendingKeys intersectSet:allKeys];
770
771 NSMutableSet *filtered =[_pendingKeys mutableCopy];
772 [filtered intersectSet:availableKeys];
773
774 return filtered;
775 }
776
777 - (NSMutableDictionary *)copyChangedValues:(NSSet*)keysOfInterest
778 {
779 // Grab values from KVS.
780 NSUbiquitousKeyValueStore *store = [self cloudStore];
781 NSMutableDictionary *changedValues = [NSMutableDictionary dictionaryWithCapacity:0];
782 [keysOfInterest enumerateObjectsUsingBlock:^(id obj, BOOL *stop)
783 {
784 NSString* key = (NSString*) obj;
785 id objval = [store objectForKey:key];
786 if (!objval) objval = [NSNull null];
787
788 [changedValues setObject:objval forKey:key];
789 secdebug(XPROXYSCOPE, "%@ storeChanged updated value for %@", self, key);
790 }];
791 return changedValues;
792 }
793
794 /*
795 During RegisterKeys, separate keys-of-interest into three disjoint sets:
796 - keys that we always want to be notified about; this means we can get the
797 value at any time
798 - keys that require the device to have been unlocked at least once
799 - keys that require the device to be unlocked now
800
801 Typically, the sets of keys will be:
802
803 - Dk: alwaysKeys
804 - Ck: firstUnlock
805 - Ak: unlocked
806
807 The caller is responsible for making sure that the keys in e.g. alwaysKeys are
808 values that can be handled at any time (that is, not when unlocked)
809
810 Each time we get a notification from ubiquity that keys have changed, we need to
811 see if anything of interest changed. If we don't care, then done.
812
813 For each key-of-interest that changed, we either notify the client that things
814 changed, or add it to a pendingNotifications list. If the notification to the
815 client fails, also add it to the pendingNotifications list. This pending list
816 should be written to persistent storage and consulted any time we either get an
817 item changed notification, or get a stream event signalling a change in lock state.
818
819 We can notify the client either through XPC if a connection is set up, or call a
820 routine in securityd to launch it.
821
822 */
823
824 - (void)processKeyChangedEvent:(NSDictionary *)changedValues
825 {
826 NSMutableDictionary* filtered = [NSMutableDictionary dictionary];
827 NSMutableArray* nullKeys = [NSMutableArray array];
828 // Remove nulls because we don't want them in securityd.
829 [changedValues enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
830 if (obj == [NSNull null])
831 [nullKeys addObject:key];
832 else
833 filtered[key] = obj;
834 }];
835 if ([filtered count]) {
836 [self calloutWith:^(NSSet *pending, bool syncWithPeersPending, dispatch_queue_t queue, void(^done)(NSSet *, bool)) {
837 CFErrorRef error = NULL;
838 bool bx = [filtered count] && _SecKeychainSyncUpdate((__bridge CFDictionaryRef)filtered, &error);
839 // TODO: Make SecKeychainSyncUpdate return handled keys, for now we
840 // record that we handled everything when _SecKeychainSyncUpdate succeeds.
841 NSSet* handledKeys = [NSSet setWithArray: bx ? [changedValues allKeys] : nullKeys];
842 dispatch_async(queue, ^{
843 done(handledKeys, NO);
844 if (bx)
845 secnotice("keytrace", "%@ handled: %@ null: %@ pending: %@", self,
846 [[filtered allKeys] componentsJoinedByString: @" "],
847 [nullKeys componentsJoinedByString: @" "],
848 [[_pendingKeys allObjects] componentsJoinedByString: @" "]);
849 else
850 secerror("%@ failed: %@ not handled: %@ null: %@ pending: %@", self, error,
851 [[filtered allKeys] componentsJoinedByString: @" "],
852 [nullKeys componentsJoinedByString: @" "],
853 [[_pendingKeys allObjects] componentsJoinedByString: @" "]);
854 CFReleaseSafe(error);
855 });
856 }];
857 } else {
858 if ([nullKeys count])
859 [_pendingKeys minusSet: [NSSet setWithArray: nullKeys]];
860
861 secnotice("keytrace", "%@ handled: %@ null: %@ pending: %@", self,
862 [[filtered allKeys] componentsJoinedByString: @" "],
863 [nullKeys componentsJoinedByString: @" "],
864 [[_pendingKeys allObjects] componentsJoinedByString: @" "]);
865 }
866 }
867
868 @end
869