5 // Created by Mitch Adler on 5/15/16.
9 #import "CKDKVSStore.h"
10 #import "CKDKVSProxy.h"
12 #include "SOSCloudKeychainConstants.h"
13 #include <utilities/debugging.h>
15 #import <Foundation/NSUbiquitousKeyValueStore.h>
16 #import <Foundation/NSUbiquitousKeyValueStore_Private.h>
17 #import "SyncedDefaults/SYDConstants.h"
18 #include <os/activity.h>
20 #import "Analytics/Clients/SOSAnalytics.h"
22 #include "keychain/SecureObjectSync/SOSKVSKeys.h"
23 #include <Security/OTConstants.h>
25 struct CKDKVSCounters {
27 uint64_t synchronizeWithCompletionHandler;
28 uint64_t incomingMessages;
29 uint64_t outgoingMessages;
30 uint64_t totalWaittimeSynchronize;
31 uint64_t longestWaittimeSynchronize;
32 uint64_t synchronizeFailures;
35 @interface CKDKVSStore ()
36 @property (readwrite, weak) UbiqitousKVSProxy* proxy;
37 @property (readwrite) NSUbiquitousKeyValueStore* cloudStore;
38 @property (assign,readwrite) struct CKDKVSCounters* perfCounters;
39 @property dispatch_queue_t perfQueue;
42 @implementation CKDKVSStore
44 + (instancetype)kvsInterface {
45 return [[CKDKVSStore alloc] init];
48 - (instancetype)init {
49 if ((self = [super init])) {
51 self->_cloudStore = [NSUbiquitousKeyValueStore defaultStore];
54 if (!self.cloudStore) {
55 secerror("NO NSUbiquitousKeyValueStore defaultStore!!!");
58 self.perfQueue = dispatch_queue_create("CKDKVSStorePerfQueue", NULL);
59 self.perfCounters = calloc(1, sizeof(struct CKDKVSCounters));
71 - (void) connectToProxy: (UbiqitousKVSProxy*) proxy {
74 [[NSNotificationCenter defaultCenter] addObserver:self
75 selector:@selector(kvsStoreChangedAsync:)
76 name:NSUbiquitousKeyValueStoreDidChangeExternallyNotification
81 - (void)setObject:(id)obj forKey:(NSString*)key {
82 secdebug("kvsdebug", "%@ key %@ set to: %@ from: %@", self, key, obj, [self.cloudStore objectForKey:key]);
83 [self.cloudStore setObject:obj forKey:key];
86 - (NSDictionary<NSString *, id>*) copyAsDictionary {
87 return [self.cloudStore dictionaryRepresentation];
90 - (void)addEntriesFromDictionary:(NSDictionary<NSString*, NSObject*> *)otherDictionary {
91 [otherDictionary enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, NSObject * _Nonnull obj, BOOL * _Nonnull stop) {
92 [self setObject:obj forKey:key];
96 - (id)objectForKey:(NSString*)key {
97 return [self.cloudStore objectForKey:key];
100 - (void)removeObjectForKey:(NSString*)key {
101 return [self.cloudStore removeObjectForKey:key];
104 - (void)removeAllObjects {
105 [[[[self.cloudStore dictionaryRepresentation] allKeys] copy] enumerateObjectsUsingBlock:^(NSString * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
106 [self.cloudStore removeObjectForKey:obj];
111 - (void)forceSynchronizeWithKVS
113 secnoticeq("pushWrites", "requesting force synchronization with KVS on CloudKit");
115 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
116 NSError *error = nil;
117 bool success = [self pullUpdates:&error];
118 if(!success || error != nil) {
119 secerror("pushWrites: failed to synchronize with KVS: %@", error);
121 secnoticeq("pushWrites", "successfully synced with KVS!");
124 dispatch_async(self.perfQueue, ^{
125 self.perfCounters->synchronize++;
129 - (void)pushWrites:(NSArray<NSString*>*)keys requiresForceSync:(BOOL)requiresForceSync
131 secnoticeq("pushWrites", "Push writes");
133 if (SecKVSOnCloudKitIsEnabled() == NO) {
134 secnoticeq("pushWrites", "KVS on CloudKit not enabled");
136 [[self cloudStore] synchronize];
137 dispatch_async(self.perfQueue, ^{
138 self.perfCounters->synchronize++;
143 if(requiresForceSync == YES) {
144 secnoticeq("pushWrites", "requested to force synchronize");
145 [self forceSynchronizeWithKVS];
149 //if KVS on CK is enabled we should only force sync rings, circles, and key parameters
150 secnoticeq("pushWrites", "KVS on CloudKit enabled. Evaluating changed keys");
152 if (keys == nil || [keys count] == 0){
153 secnoticeq("pushWrites", "key set is empty, returning");
157 __block BOOL proceedWithSync = NO;
158 [keys enumerateObjectsUsingBlock:^(NSString *kvsKey, NSUInteger idx, BOOL *stop) {
159 if ([kvsKey containsString:(__bridge_transfer NSString*)sRingPrefix] ||
160 [kvsKey containsString:(__bridge_transfer NSString*)sCirclePrefix] ||
161 [kvsKey containsString:(__bridge_transfer NSString*)kSOSKVSKeyParametersKey]) {
162 proceedWithSync = YES;
166 if (proceedWithSync == NO) {
167 secnoticeq("pushWrites", "no keys to force push, returning");
171 [self forceSynchronizeWithKVS];
174 // Runs on the same thread that posted the notification, and that thread _may_ be the
175 // kdkvsproxy_queue (see 30470419). Avoid deadlock by bouncing through global queue.
176 - (void)kvsStoreChangedAsync:(NSNotification *)notification
178 secnotice("CloudKeychainProxy", "%@ KVS Remote changed notification: %@", self, notification);
179 dispatch_async(self.perfQueue, ^{
180 self.perfCounters->incomingMessages++;
182 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
183 [self kvsStoreChanged:notification];
187 - (void) kvsStoreChanged:(NSNotification *)notification {
189 Posted when the value of one or more keys in the local key-value store
190 changed due to incoming data pushed from iCloud. This notification is
191 sent only upon a change received from iCloud; it is not sent when your
194 The user info dictionary can contain the reason for the notification as
195 well as a list of which values changed, as follows:
197 The value of the NSUbiquitousKeyValueStoreChangeReasonKey key, when
198 present, indicates why the key-value store changed. Its value is one of
199 the constants in "Change Reason Values."
201 The value of the NSUbiquitousKeyValueStoreChangedKeysKey, when present,
202 is an array of strings, each the name of a key whose value changed. The
203 notification object is the NSUbiquitousKeyValueStore object whose contents
206 NSUbiquitousKeyValueStoreInitialSyncChange is only posted if there is any
207 local value that has been overwritten by a distant value. If there is no
208 conflict between the local and the distant values when doing the initial
209 sync (e.g. if the cloud has no data stored or the client has not stored
210 any data yet), you'll never see that notification.
212 NSUbiquitousKeyValueStoreInitialSyncChange implies an initial round trip
213 with server but initial round trip with server does not imply
214 NSUbiquitousKeyValueStoreInitialSyncChange.
216 os_activity_initiate("cloudChanged", OS_ACTIVITY_FLAG_DEFAULT, ^{
217 secdebug(XPROXYSCOPE, "%@ KVS Remote changed notification: %@", self, notification);
219 UbiqitousKVSProxy* proxy = self.proxy;
221 // Weak reference went away.
225 NSDictionary *userInfo = [notification userInfo];
226 NSNumber *reason = [userInfo objectForKey:NSUbiquitousKeyValueStoreChangeReasonKey];
227 NSArray *keysChangedArray = [userInfo objectForKey:NSUbiquitousKeyValueStoreChangedKeysKey];
228 NSSet *keysChanged = keysChangedArray ? [NSSet setWithArray: keysChangedArray] : nil;
230 if (reason) switch ([reason integerValue]) {
231 case NSUbiquitousKeyValueStoreInitialSyncChange:
232 [proxy storeKeysChanged: keysChanged initial: YES];
235 case NSUbiquitousKeyValueStoreServerChange:
236 [proxy storeKeysChanged: keysChanged initial: NO];
239 case NSUbiquitousKeyValueStoreQuotaViolationChange:
240 seccritical("Received NSUbiquitousKeyValueStoreQuotaViolationChange");
243 case NSUbiquitousKeyValueStoreAccountChange:
244 [proxy storeAccountChanged];
248 secinfo("kvsstore", "ignoring unknown notification: %@", reason);
254 - (BOOL) pullUpdates:(NSError **)failure
256 __block NSError *tempFailure = nil;
258 dispatch_semaphore_t freshSemaphore = dispatch_semaphore_create(0);
260 secnoticeq("fresh", "%s CALLING OUT TO syncdefaultsd SWCH: %@", kWAIT2MINID, self);
262 dispatch_async(self.perfQueue, ^{
263 self.perfCounters->synchronizeWithCompletionHandler++;
266 [[self cloudStore] synchronizeWithCompletionHandler:^(NSError *error) {
268 dispatch_async(self.perfQueue, ^{
269 self.perfCounters->synchronizeFailures++;
272 secnotice("fresh", "%s RETURNING FROM syncdefaultsd SWCH: %@: %@", kWAIT2MINID, self, error);
274 dispatch_async(self.perfQueue, ^{
275 self.perfCounters->synchronize++;
277 secnotice("fresh", "%s RETURNING FROM syncdefaultsd SWCH: %@", kWAIT2MINID, self);
278 if(SecKVSOnCloudKitIsEnabled() == NO) {
279 [[self cloudStore] synchronize]; // Per olivier in <rdar://problem/13412631>, sync before getting values
280 secnotice("fresh", "%s RETURNING FROM syncdefaultsd SYNC: %@", kWAIT2MINID, self);
283 dispatch_semaphore_signal(freshSemaphore);
285 dispatch_semaphore_wait(freshSemaphore, DISPATCH_TIME_FOREVER);
287 if (failure && (*failure == NULL)) {
288 *failure = tempFailure;
291 return tempFailure == nil;
294 - (void)perfCounters:(void(^)(NSDictionary *counters))callback
296 dispatch_async(self.perfQueue, ^{
298 CKDKVSPerfCounterSynchronize : @(self.perfCounters->synchronize),
299 CKDKVSPerfCounterSynchronizeWithCompletionHandler : @(self.perfCounters->synchronizeWithCompletionHandler),
300 CKDKVSPerfCounterIncomingMessages : @(self.perfCounters->incomingMessages),
301 CKDKVSPerfCounterOutgoingMessages : @(self.perfCounters->outgoingMessages),
302 CKDKVSPerfCounterTotalWaitTimeSynchronize : @(self.perfCounters->totalWaittimeSynchronize),
303 CKDKVSPerfCounterLongestWaitTimeSynchronize : @(self.perfCounters->longestWaittimeSynchronize),
304 CKDKVSPerfCounterSynchronizeFailures : @(self.perfCounters->synchronizeFailures),
310 - (void)setupSamplers
312 [[SOSAnalytics logger] AddMultiSamplerForName:CKDKVSPerformanceCountersSampler
313 withTimeInterval:SFAnalyticsSamplerIntervalOncePerReport
314 block:^NSDictionary<NSString *,NSNumber *> *{
315 __block NSDictionary<NSString *,NSNumber *>* data;
316 [self perfCounters:^(NSDictionary *counters) {
320 dispatch_sync(self.perfQueue, ^{
321 memset(self.perfCounters, 0, sizeof(struct CKDKVSCounters));
327 - (void)setupSamplers
329 // SFA is only for 64 bit cool kids
333 - (void)addOneToOutGoing
335 dispatch_async(self.perfQueue, ^{
336 self.perfCounters->outgoingMessages++;