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 63static const char *kStreamName = "com.apple.notifyd.matching"; 64 65 66static NSString *kKeyAlwaysKeys = @"AlwaysKeys"; 67static NSString *kKeyFirstUnlockKeys = @"FirstUnlockKeys"; 68static NSString *kKeyUnlockedKeys = @"UnlockedKeys"; 69static NSString *kKeyPendingKeys = @"PendingKeys"; 70static NSString *kKeyUnsentChangedKeys = @"unsentChangedKeys"; 71static NSString *kKeyUnlockNotificationRequested = @"unlockNotificationRequested"; 72static NSString *kKeySyncWithPeersPending = @"SyncWithPeersPending"; 73 74enum 75{ 76 kCallbackMethodSecurityd = 0, 77 kCallbackMethodXPC = 1, 78}; 79 80static const int64_t kMinSyncDelay = (NSEC_PER_MSEC * 500); 81static const int64_t kMaxSyncDelay = (NSEC_PER_SEC * 5); 82static const int64_t kMinSyncInterval = (NSEC_PER_SEC * 15); 83static 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 870