2 * Copyright (c) 2012-2013 Apple Computer, 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@
29 #import <Foundation/NSUbiquitousKeyValueStore.h>
30 #import <Foundation/NSUbiquitousKeyValueStore_Private.h>
31 #import <Foundation/NSArray.h>
32 #import <Foundation/Foundation.h>
34 #import <Security/SecBasePriv.h>
35 #import <Security/SecItemPriv.h>
36 #import <utilities/debugging.h>
39 #import "CKDKVSProxy.h"
40 #import "CKDPersistentState.h"
41 #import "CKDUserInteraction.h"
43 #import "SOSARCDefines.h"
45 #include <SecureObjectSync/SOSAccount.h>
46 #include <SecureObjectSync/SOSCloudCircleInternal.h>
47 #include "SOSCloudKeychainConstants.h"
49 #include <utilities/SecAKSWrappers.h>
50 #include <utilities/SecCFRelease.h>
52 #define SOSCKCSCOPE "sync"
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.
63 static const char *kStreamName = "com.apple.notifyd.matching";
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";
76 kCallbackMethodSecurityd = 0,
77 kCallbackMethodXPC = 1,
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);
85 @interface NSUbiquitousKeyValueStore (NSUbiquitousKeyValueStore_PrivateZ)
86 - (void) _synchronizeWithCompletionHandler:(void (^)(NSError *error))completionHandler;
90 - (void) synchronizeWithCompletionHandler:(void (^)(NSError *error))completionHandler;
95 @implementation UbiqitousKVSProxy
100 [SOSPersistentState setRegisteredKeys:[self exportKeyInterests]];
103 + (UbiqitousKVSProxy *) sharedKVSProxy
105 static UbiqitousKVSProxy *sharedKVSProxy;
106 if (!sharedKVSProxy) {
107 static dispatch_once_t onceToken;
108 dispatch_once(&onceToken, ^{
109 sharedKVSProxy = [[self alloc] init];
112 return sharedKVSProxy;
117 if (self = [super init])
119 secnotice("event", "%@ start", self);
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, ^{
126 dispatch_resume(_syncTimer);
128 [[NSNotificationCenter defaultCenter]
130 selector: @selector (iCloudAccountAvailabilityChanged:)
131 name: NSUbiquityIdentityDidChangeNotification
134 [[NSNotificationCenter defaultCenter] addObserver:self
135 selector:@selector(cloudChanged:)
136 name:NSUbiquitousKeyValueStoreDidChangeExternallyNotification
139 [self importKeyInterests: [SOSPersistentState registeredKeys]];
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];
147 [self updateUnlockedSinceBoot];
148 [self updateIsLocked];
150 [self keybagDidUnlock];
152 secdebug(XPROXYSCOPE, "%@ done", self);
157 - (NSString *)description
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" : "-"];
171 - (void)processAllItems
173 NSDictionary *allItems = [self getAll];
176 secnotice("event", "%@ sending: %@", self, [[allItems allKeys] componentsJoinedByString: @" "]);
177 [self processKeyChangedEvent:allItems];
180 secdebug(XPROXYSCOPE, "%@ No items in KVS", self);
183 - (void)setItemsChangedBlock:(CloudItemsChangedBlock)itemsChangedBlock
185 self->itemsChangedCallback = itemsChangedBlock;
190 secdebug(XPROXYSCOPE, "%@", self);
191 [[NSNotificationCenter defaultCenter] removeObserver:self
192 name:NSUbiquitousKeyValueStoreDidChangeExternallyNotification object:nil];
194 [[NSNotificationCenter defaultCenter] removeObserver:self
195 name:NSUbiquityIdentityDidChangeNotification object:nil];
198 // MARK: ----- Client Interface -----
200 - (void)setObject:(id)obj forKey:(id)key
202 secdebug("keytrace", "%@ key: %@, obj: %@", self, key, obj);
203 NSUbiquitousKeyValueStore *store = [self cloudStore];
206 id value = [store objectForKey:key];
208 secdebug("keytrace", "%@ Current value (before set) for key %@ is: %@", self, key, value);
210 secdebug("keytrace", "%@ No current value for key %@", self, key);
213 secdebug("keytrace", "Can't get store");
215 [[self cloudStore] setObject:obj forKey:key];
216 [self requestSynchronization:NO];
219 - (void)setObjectsFromDictionary:(NSDictionary *)values
221 secdebug(XPROXYSCOPE, "%@ start: %lu values to put", self, (unsigned long)[values count]);
223 NSUbiquitousKeyValueStore *store = [self cloudStore];
226 [values enumerateKeysAndObjectsUsingBlock: ^(id key, id obj, BOOL *stop)
228 if (obj == NULL || obj == [NSNull null])
229 [store removeObjectForKey:key];
231 [store setObject:obj forKey:key];
234 [self requestSynchronization:NO];
237 secdebug(XPROXYSCOPE, "%@ NULL? store: %@, values: %@", self, store, values);
240 - (void)requestSynchronization:(bool)force
244 secdebug(XPROXYSCOPE, "%@ synchronize (forced)", self);
245 [[self cloudStore] synchronize];
249 secdebug(XPROXYSCOPE, "%@ synchronize (soon)", self);
250 [[self cloudStore] synchronize];
254 - (NSUbiquitousKeyValueStore *)cloudStore
256 NSUbiquitousKeyValueStore *iCloudStore = [NSUbiquitousKeyValueStore defaultStore];
257 // secdebug(XPROXYSCOPE, "cloudStore: %@", iCloudStore);
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
264 secdebug(XPROXYSCOPE, "%@ synchronize and wait", self);
268 NSError *err = [NSError errorWithDomain:(NSString *)NSPOSIXErrorDomain code:(NSInteger)ENOPROTOOPT userInfo:nil];
273 secdebug(XPROXYSCOPE, "%@ Requesting synchronizeWithCompletionHandler", self);
275 [[self cloudStore] synchronizeWithCompletionHandler:^(NSError *error) {
276 [[self cloudStore] synchronize]; // Per olivier in <rdar://problem/13412631>, sync before getting values
278 NSDictionary * changedKeys = error ? @{} : [self copyChangedValues:[NSSet setWithArray:keys]];
280 secdebug(XPROXYSCOPE, "%@ Got synchronize completion %@ (error %@)", self, changedKeys, error);
281 handler(changedKeys, error);
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);
293 - (void)removeObjectForKey:(NSString *)keyToRemove
295 [[self cloudStore] removeObjectForKey:keyToRemove];
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)
305 secdebug(XPROXYSCOPE, "%@ Clearing value for key %@", self, key);
306 [[self cloudStore] removeObjectForKey:(NSString *)key];
309 [self requestSynchronization:YES];
314 // MARK: ----- KVS key lists -----
319 return [[self cloudStore] objectForKey:key];
322 - (NSDictionary *)getAll
324 return [[self cloudStore] dictionaryRepresentation];
327 - (NSDictionary*) exportKeyInterests
329 return @{ kKeyAlwaysKeys:[_alwaysKeys allObjects],
330 kKeyFirstUnlockKeys:[_firstUnlockKeys allObjects],
331 kKeyUnlockedKeys:[_unlockedKeys allObjects],
332 kKeyPendingKeys:[_pendingKeys allObjects],
333 kKeySyncWithPeersPending:[NSNumber numberWithBool:_syncWithPeersPending]
337 - (void) importKeyInterests: (NSDictionary*) interests
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];
346 - (NSMutableSet *)copyAllKeys
348 NSMutableSet *allKeys = [NSMutableSet setWithSet: _alwaysKeys];
349 [allKeys unionSet: _firstUnlockKeys];
350 [allKeys unionSet: _unlockedKeys];
354 - (NSDictionary *)registerKeysAndGet:(BOOL)getNewKeysOnly always:(NSArray *)keysToRegister reqFirstUnlock:(NSArray *)reqFirstUnlock reqUnlocked:(NSArray *)reqUnlocked
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];
362 [_pendingKeys intersectSet:allNewKeys];
364 NSSet* keysToGet = nil;
365 if (getNewKeysOnly) {
366 [allNewKeys minusSet:allOldKeys];
367 keysToGet = allNewKeys;
369 keysToGet = [self keysForCurrentLockState];
372 // Mark keysToGet for the current lockState as pending
373 NSSet *filteredKeys = [self filterKeySetWithLockState:keysToGet];
375 NSMutableDictionary *returnedValues = [self copyChangedValues: filteredKeys];
377 secnotice("keytrace", "%@ [%s] always: %@ firstUnlock: %@ unlocked: %@ got: %@", self, getNewKeysOnly ? "new only " : "", [keysToRegister componentsJoinedByString: @" "], [reqFirstUnlock componentsJoinedByString: @" "], [reqUnlocked componentsJoinedByString: @" "], [[returnedValues allKeys] componentsJoinedByString: @" "]);
379 [self processKeyChangedEvent:returnedValues];
380 [returnedValues removeAllObjects];
382 return returnedValues;
385 - (NSDictionary *)localNotification:(NSDictionary *)localNotificationDict outFlags:(int64_t *)outFlags
387 __block bool done = false;
388 __block int64_t returnedFlags = 0;
389 __block NSDictionary *responses = NULL;
390 typedef bool (^CKDUserInteractionBlock) (CFDictionaryRef responses);
392 CKDUserInteraction *cui = [CKDUserInteraction sharedInstance];
393 [cui requestShowNotification:localNotificationDict completion:^ bool (CFDictionaryRef userResponses, int64_t flags)
395 responses = [NSDictionary dictionaryWithDictionary:(__bridge NSDictionary *)userResponses];
396 returnedFlags = flags;
397 secdebug(XPROXYSCOPE, "%@ requestShowNotification: dict: %@, flags: %#llx", self, responses, returnedFlags);
402 // TODO: replace with e.g. dispatch calls to wait, or semaphore
407 secdebug(XPROXYSCOPE, "%@ outFlags: %#llx", self, returnedFlags);
408 *outFlags = returnedFlags;
413 - (void)setParams:(NSDictionary *)paramsDict
415 secdebug(XPROXYSCOPE, "%@ setParams: %@ ", self, paramsDict);
416 NSString *cbmethod = [paramsDict objectForKey:(__bridge id)kParamCallbackMethod];
420 if ([cbmethod isEqualToString:(__bridge NSString *)kParamCallbackMethodSecurityd])
421 callbackMethod = kCallbackMethodSecurityd;
422 else if ([cbmethod isEqualToString:(__bridge NSString *)kParamCallbackMethodXPC])
423 callbackMethod = kCallbackMethodXPC;
426 - (void)saveToUbiquitousStore
428 [self requestSynchronization:NO];
431 // MARK: ----- Event Handling -----
433 - (void)streamEvent:(xpc_object_t)notification
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];
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);
454 - (void)iCloudAccountAvailabilityChanged:(NSNotification*)notification
457 Continuing this example, you’d then implement an iCloudAccountAvailabilityChanged: method that would:
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.
464 id previCloudToken = currentiCloudToken;
465 currentiCloudToken = [[NSFileManager defaultManager] ubiquityIdentityToken];
466 if (previCloudToken != currentiCloudToken)
467 secnotice("event", "%@ iCloud account changed!", self);
469 secnotice("event", "%@ %@", self, notification);
472 - (void)cloudChanged:(NSNotification*)notification
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
480 The user info dictionary can contain the reason for the notification as
481 well as a list of which values changed, as follows:
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."
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
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.
498 NSUbiquitousKeyValueStoreInitialSyncChange implies an initial round trip
499 with server but initial round trip with server does not imply
500 NSUbiquitousKeyValueStoreInitialSyncChange.
503 secdebug(XPROXYSCOPE, "%@ cloudChanged notification: %@", self, notification);
505 NSDictionary *userInfo = [notification userInfo];
506 NSNumber *reason = [userInfo objectForKey:NSUbiquitousKeyValueStoreChangeReasonKey];
507 if (reason) switch ([reason integerValue]) {
508 case NSUbiquitousKeyValueStoreInitialSyncChange:
509 case NSUbiquitousKeyValueStoreServerChange:
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";
518 secnotice("event", "%@ keysChangedInCloud: %@ keysOfInterest: %@", self, [[keysChangedInCloud allObjects] componentsJoinedByString: @" "], [[changedValues allKeys] componentsJoinedByString: @" "]);
519 if ([changedValues count])
520 [self processKeyChangedEvent:changedValues];
523 case NSUbiquitousKeyValueStoreQuotaViolationChange:
524 seccritical("%@ event received NSUbiquitousKeyValueStoreQuotaViolationChange", self);
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)
531 NSDictionary *changedValues = @{ (__bridge NSString*)kSOSKVSAccountChangedKey: @"true" };
532 [self processKeyChangedEvent:changedValues];
538 - (void) calloutWith: (void(^)(NSSet *pending, bool syncWithPeersPending, dispatch_queue_t queue, void(^done)(NSSet *handledKeys, bool handledSyncWithPeers))) callout
540 // In CKDKVSProxy's serial queue
541 NSSet *myPending = [_pendingKeys copy];
542 bool mySyncWithPeersPending = _syncWithPeersPending;
543 bool wasLocked = _isLocked;
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
552 // Update SyncWithPeers stuff.
553 _syncWithPeersPending = (mySyncWithPeersPending && (!handledSyncWithPeers)) || _shadowSyncWithPeersPending;
554 _shadowSyncWithPeersPending = NO;
555 if (handledSyncWithPeers)
556 _lastSyncTime = dispatch_time(DISPATCH_TIME_NOW, 0);
558 // Update pendingKeys and handle them
559 [_pendingKeys minusSet: handledKeys];
560 bool hadShadowPendingKeys = [_shadowPendingKeys count];
561 NSSet *filteredKeys = [self filterKeySetWithLockState:_shadowPendingKeys];
562 _shadowPendingKeys = nil;
564 // Write state to disk
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]];
583 - (void) doSyncWithAllPeers
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);
603 secerror("%@ syncWithAllPeers %@, unknown reason: %d", self, error, reason);
606 done(nil, handledSyncWithPeers);
607 CFReleaseSafe(error);
614 secnotice("event", "%@ syncWithPeersPending: %d inCallout: %d isLocked: %d", self, _syncWithPeersPending, _inCallout, _isLocked);
615 _syncTimerScheduled = NO;
616 if (_syncWithPeersPending && !_inCallout && !_isLocked)
617 [self doSyncWithAllPeers];
620 - (dispatch_time_t) nextSyncTime
622 dispatch_time_t nextSync = dispatch_time(DISPATCH_TIME_NOW, kMinSyncDelay);
624 // Don't sync again unless we waited at least kMinSyncInterval
626 dispatch_time_t soonest = dispatch_time(_lastSyncTime, kMinSyncInterval);
627 if (nextSync < soonest || _deadline < soonest) {
628 secdebug("timer", "%@ backing off", self);
633 // Don't delay more than kMaxSyncDelay after the first request.
634 if (nextSync > _deadline) {
635 secdebug("timer", "%@ hit deadline", self);
639 // Bump the timer by kMinSyncDelay
640 if (_syncTimerScheduled)
641 secdebug("timer", "%@ bumped timer", self);
643 secdebug("timer", "%@ scheduled timer", self);
648 - (void)scheduleSyncRequestTimer
650 dispatch_source_set_timer(_syncTimer, [self nextSyncTime], DISPATCH_TIME_FOREVER, kSyncTimerLeeway);
651 _syncTimerScheduled = YES;
654 - (void)requestSyncWithAllPeers // secd calling SOSCCSyncWithAllPeers invokes this
656 secdebug("event", "%@ _syncWithPeersPending: %d _inCallout: %d _shadowSyncWithPeersPending: %d _isLocked: %d", self, _syncWithPeersPending, _inCallout, _shadowSyncWithPeersPending, _isLocked);
658 if (!_syncWithPeersPending || (_inCallout && !_shadowSyncWithPeersPending))
659 _deadline = dispatch_time(DISPATCH_TIME_NOW, kMaxSyncDelay);
661 if (!_syncWithPeersPending) {
662 _syncWithPeersPending = YES;
667 _shadowSyncWithPeersPending = YES;
670 [self scheduleSyncRequestTimer];
673 - (BOOL) updateUnlockedSinceBoot
675 CFErrorRef aksError = NULL;
676 if (!SecAKSGetHasBeenUnlocked(&_unlockedSinceBoot, &aksError)) {
677 secerror("%@ Got error from SecAKSGetHasBeenUnlocked: %@", self, aksError);
678 CFReleaseSafe(aksError);
684 - (BOOL) updateIsLocked
686 CFErrorRef aksError = NULL;
687 if (!SecAKSGetIsLocked(&_isLocked, &aksError)) {
688 secerror("%@ Got error querying lock state: %@", self, aksError);
689 CFReleaseSafe(aksError);
693 _unlockedSinceBoot = YES;
697 - (void) keybagStateChange
699 BOOL wasLocked = _isLocked;
700 if ([self updateIsLocked]) {
701 if (wasLocked == _isLocked)
702 secdebug("event", "%@ still %s ignoring", self, _isLocked ? "locked" : "unlocked");
704 [self keybagDidLock];
706 [self keybagDidUnlock];
710 - (void) keybagDidLock
712 secnotice("event", "%@", self);
715 - (void) keybagDidUnlock
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]];
722 // Then, tickle securityd to perform a sync if needed.
723 if (_syncWithPeersPending && !_syncTimerScheduled) {
724 [self doSyncWithAllPeers];
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]];
738 secdebug("event", "%@ ignored, waiting for NSNotification", self);
743 // MARK: ----- Key Filtering -----
746 - (NSSet*) keysForCurrentLockState
748 secdebug("keytrace", "%@ Filtering: unlockedSinceBoot: %d\n unlocked: %d\n, _keysOfInterest: %@", self, (int) _unlockedSinceBoot, (int) !_isLocked, [self exportKeyInterests]);
750 NSMutableSet *currentStateKeys = [NSMutableSet setWithSet: _alwaysKeys];
751 if (_unlockedSinceBoot)
752 [currentStateKeys unionSet: _firstUnlockKeys];
755 [currentStateKeys unionSet: _unlockedKeys];
757 return currentStateKeys;
760 - (NSSet*) filterKeySetWithLockState: (NSSet*) startingSet
762 NSSet* availableKeys = [self keysForCurrentLockState];
763 NSSet *allKeys = [self copyAllKeys];
765 [_shadowPendingKeys unionSet:startingSet];
766 [_shadowPendingKeys intersectSet:allKeys];
768 [_pendingKeys unionSet:startingSet];
769 [_pendingKeys intersectSet:allKeys];
771 NSMutableSet *filtered =[_pendingKeys mutableCopy];
772 [filtered intersectSet:availableKeys];
777 - (NSMutableDictionary *)copyChangedValues:(NSSet*)keysOfInterest
779 // Grab values from KVS.
780 NSUbiquitousKeyValueStore *store = [self cloudStore];
781 NSMutableDictionary *changedValues = [NSMutableDictionary dictionaryWithCapacity:0];
782 [keysOfInterest enumerateObjectsUsingBlock:^(id obj, BOOL *stop)
784 NSString* key = (NSString*) obj;
785 id objval = [store objectForKey:key];
786 if (!objval) objval = [NSNull null];
788 [changedValues setObject:objval forKey:key];
789 secdebug(XPROXYSCOPE, "%@ storeChanged updated value for %@", self, key);
791 return changedValues;
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
798 - keys that require the device to have been unlocked at least once
799 - keys that require the device to be unlocked now
801 Typically, the sets of keys will be:
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)
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.
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.
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.
824 - (void)processKeyChangedEvent:(NSDictionary *)changedValues
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];
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);
845 secnotice("keytrace", "%@ handled: %@ null: %@ pending: %@", self,
846 [[filtered allKeys] componentsJoinedByString: @" "],
847 [nullKeys componentsJoinedByString: @" "],
848 [[_pendingKeys allObjects] componentsJoinedByString: @" "]);
850 secerror("%@ failed: %@ not handled: %@ null: %@ pending: %@", self, error,
851 [[filtered allKeys] componentsJoinedByString: @" "],
852 [nullKeys componentsJoinedByString: @" "],
853 [[_pendingKeys allObjects] componentsJoinedByString: @" "]);
854 CFReleaseSafe(error);
858 if ([nullKeys count])
859 [_pendingKeys minusSet: [NSSet setWithArray: nullKeys]];
861 secnotice("keytrace", "%@ handled: %@ null: %@ pending: %@", self,
862 [[filtered allKeys] componentsJoinedByString: @" "],
863 [nullKeys componentsJoinedByString: @" "],
864 [[_pendingKeys allObjects] componentsJoinedByString: @" "]);