]> git.saurik.com Git - apple/security.git/blob - keychain/ckks/tests/MockCloudKit.m
Security-58286.1.32.tar.gz
[apple/security.git] / keychain / ckks / tests / MockCloudKit.m
1 /*
2 * Copyright (c) 2016 Apple Inc. All Rights Reserved.
3 *
4 * @APPLE_LICENSE_HEADER_START@
5 *
6 * This file contains Original Code and/or Modifications of Original Code
7 * as defined in and that are subject to the Apple Public Source License
8 * Version 2.0 (the 'License'). You may not use this file except in
9 * compliance with the License. Please obtain a copy of the License at
10 * http://www.opensource.apple.com/apsl/ and read it before using this
11 * file.
12 *
13 * The Original Code and all software distributed under the License are
14 * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
15 * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
16 * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
17 * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
18 * Please see the License for the specific language governing rights and
19 * limitations under the License.
20 *
21 * @APPLE_LICENSE_HEADER_END@
22 */
23
24 #if OCTAGON
25
26 #import "keychain/ckks/tests/MockCloudKit.h"
27 #import "keychain/ckks/CKKS.h"
28 #import "keychain/ckks/CKKSRecordHolder.h"
29
30 #import <CloudKit/CloudKit.h>
31 #import <CloudKit/CloudKit_Private.h>
32 #include <security_utilities/debugging.h>
33 #import <Foundation/Foundation.h>
34
35
36 @implementation FakeCKModifyRecordZonesOperation
37 @synthesize database = _database;
38 @synthesize recordZonesToSave = _recordZonesToSave;
39 @synthesize recordZoneIDsToDelete = _recordZoneIDsToDelete;
40 @synthesize modifyRecordZonesCompletionBlock = _modifyRecordZonesCompletionBlock;
41
42 - (instancetype)initWithRecordZonesToSave:(nullable NSArray<CKRecordZone *> *)recordZonesToSave recordZoneIDsToDelete:(nullable NSArray<CKRecordZoneID *> *)recordZoneIDsToDelete {
43 if(self = [super init]) {
44 _recordZonesToSave = recordZonesToSave;
45 _recordZoneIDsToDelete = recordZoneIDsToDelete;
46 _modifyRecordZonesCompletionBlock = nil;
47
48 _recordZonesSaved = nil;
49 _recordZoneIDsDeleted = nil;
50 _creationError = nil;
51
52 __weak __typeof(self) weakSelf = self;
53 self.completionBlock = ^{
54 __strong __typeof(weakSelf) strongSelf = weakSelf;
55 if(!strongSelf) {
56 secerror("ckks: received callback for released object");
57 return;
58 }
59
60 strongSelf.modifyRecordZonesCompletionBlock(strongSelf.recordZonesSaved, strongSelf.recordZoneIDsDeleted, strongSelf.creationError);
61 };
62 }
63 return self;
64 }
65
66 -(void)main {
67 // Create the zones we want; delete the ones we don't
68 // No error handling whatsoever
69 FakeCKDatabase* ckdb = [FakeCKModifyRecordZonesOperation ckdb];
70
71 for(CKRecordZone* zone in self.recordZonesToSave) {
72 bool skipCreation = false;
73 FakeCKZone* fakezone = ckdb[zone.zoneID];
74 if(fakezone.failCreationSilently) {
75 // Don't report an error, but do delete the zone
76 ckdb[zone.zoneID] = nil;
77 skipCreation = true;
78
79 } else if(fakezone.creationError) {
80
81 // Not the best way to do this, but it's an error
82 // Needs fixing if you want to support multiple zone failures
83 self.creationError = fakezone.creationError;
84
85 // 'clear' the error
86 ckdb[zone.zoneID] = nil;
87 skipCreation = true;
88
89 } else if(fakezone) {
90 continue;
91 }
92
93 if(!skipCreation) {
94 // Create the zone:
95 secnotice("ckks", "Creating zone %@", zone);
96 ckdb[zone.zoneID] = [[FakeCKZone alloc] initZone: zone.zoneID];
97 }
98
99 if(!self.recordZonesSaved) {
100 self.recordZonesSaved = [[NSMutableArray alloc] init];
101 }
102 [self.recordZonesSaved addObject:zone];
103 }
104
105 for(CKRecordZoneID* zoneID in self.recordZoneIDsToDelete) {
106 ckdb[zoneID] = nil;
107
108 if(!self.recordZoneIDsDeleted) {
109 self.recordZoneIDsDeleted = [[NSMutableArray alloc] init];
110 }
111 [self.recordZoneIDsDeleted addObject:zoneID];
112 }
113 }
114
115 +(FakeCKDatabase*) ckdb {
116 // Shouldn't ever be called: must be mocked out.
117 @throw [NSException exceptionWithName:NSInternalInconsistencyException
118 reason:[NSString stringWithFormat:@"+ckdb[] must be mocked out for use"]
119 userInfo:nil];
120 }
121 @end
122
123 @implementation FakeCKModifySubscriptionsOperation
124 @synthesize database = _database;
125 @synthesize group = _group;
126 @synthesize subscriptionsToSave = _subscriptionsToSave;
127 @synthesize subscriptionIDsToDelete = _subscriptionIDsToDelete;
128 @synthesize modifySubscriptionsCompletionBlock = _modifySubscriptionsCompletionBlock;
129
130 - (instancetype)initWithSubscriptionsToSave:(nullable NSArray<CKSubscription *> *)subscriptionsToSave subscriptionIDsToDelete:(nullable NSArray<NSString *> *)subscriptionIDsToDelete {
131 if(self = [super init]) {
132 _subscriptionsToSave = subscriptionsToSave;
133 _subscriptionIDsToDelete = subscriptionIDsToDelete;
134 _modifySubscriptionsCompletionBlock = nil;
135
136 __weak __typeof(self) weakSelf = self;
137 self.completionBlock = ^{
138 __strong __typeof(weakSelf) strongSelf = weakSelf;
139 if(!strongSelf) {
140 secerror("ckks: received callback for released object");
141 return;
142 }
143
144 strongSelf.modifySubscriptionsCompletionBlock(strongSelf.subscriptionsSaved, strongSelf.subscriptionIDsDeleted, strongSelf.subscriptionError);
145 };
146 }
147 return self;
148 }
149
150 -(void)main {
151 FakeCKDatabase* ckdb = [FakeCKModifySubscriptionsOperation ckdb];
152
153 // Are these CKRecordZoneSubscription? Who knows!
154 for(CKRecordZoneSubscription* subscription in self.subscriptionsToSave) {
155 FakeCKZone* fakezone = ckdb[subscription.zoneID];
156
157 if(!fakezone) {
158 // This is an error: the zone doesn't exist
159 self.subscriptionError = [[CKPrettyError alloc] initWithDomain:CKErrorDomain
160 code:CKErrorPartialFailure
161 userInfo:@{CKPartialErrorsByItemIDKey:
162 @{subscription.zoneID:[[CKPrettyError alloc] initWithDomain:CKErrorDomain
163 code:CKErrorZoneNotFound
164 userInfo:@{}]}
165 }];
166
167 } else if(fakezone.subscriptionError) {
168 // Not the best way to do this, but it's an error
169 // Needs fixing if you want to support multiple zone failures
170 self.subscriptionError = fakezone.subscriptionError;
171
172 // 'clear' the error
173 fakezone.subscriptionError = nil;
174 } else {
175 if(!self.subscriptionsSaved) {
176 self.subscriptionsSaved = [[NSMutableArray alloc] init];
177 }
178 [self.subscriptionsSaved addObject:subscription];
179 }
180 }
181
182 for(NSString* subscriptionID in self.subscriptionIDsToDelete) {
183 if(!self.subscriptionIDsDeleted) {
184 self.subscriptionIDsDeleted = [[NSMutableArray alloc] init];
185 }
186
187 [self.subscriptionIDsDeleted addObject:subscriptionID];
188 }
189 }
190
191 +(FakeCKDatabase*) ckdb {
192 // Shouldn't ever be called: must be mocked out.
193 @throw [NSException exceptionWithName:NSInternalInconsistencyException
194 reason:[NSString stringWithFormat:@"+ckdb[] must be mocked out for use"]
195 userInfo:nil];
196 }
197 @end
198
199 @implementation FakeCKFetchRecordZoneChangesOperation
200 @synthesize recordZoneIDs = _recordZoneIDs;
201 @synthesize optionsByRecordZoneID = _optionsByRecordZoneID;
202
203 @synthesize fetchAllChanges = _fetchAllChanges;
204 @synthesize recordChangedBlock = _recordChangedBlock;
205
206 @synthesize recordWithIDWasDeletedBlock = _recordWithIDWasDeletedBlock;
207 @synthesize recordZoneChangeTokensUpdatedBlock = _recordZoneChangeTokensUpdatedBlock;
208 @synthesize recordZoneFetchCompletionBlock = _recordZoneFetchCompletionBlock;
209 @synthesize fetchRecordZoneChangesCompletionBlock = _fetchRecordZoneChangesCompletionBlock;
210
211 @synthesize group = _group;
212
213 - (instancetype)initWithRecordZoneIDs:(NSArray<CKRecordZoneID *> *)recordZoneIDs optionsByRecordZoneID:(nullable NSDictionary<CKRecordZoneID *, CKFetchRecordZoneChangesOptions *> *)optionsByRecordZoneID {
214 if(self = [super init]) {
215 _recordZoneIDs = recordZoneIDs;
216 _optionsByRecordZoneID = optionsByRecordZoneID;
217 }
218 return self;
219 }
220
221 - (void)main {
222 // iterate through database, and return items that aren't in lastDatabase
223 FakeCKDatabase* ckdb = [FakeCKFetchRecordZoneChangesOperation ckdb];
224
225 for(CKRecordZoneID* zoneID in self.recordZoneIDs) {
226 FakeCKZone* zone = ckdb[zoneID];
227 if(!zone) {
228 // Only really supports a single zone failure
229 ckksnotice("fakeck", zoneID, "Fetched for a missing zone %@", zoneID);
230 NSError* zoneNotFoundError = [[CKPrettyError alloc] initWithDomain:CKErrorDomain
231 code:CKErrorZoneNotFound
232 userInfo:@{}];
233 NSError* error = [[CKPrettyError alloc] initWithDomain:CKErrorDomain
234 code:CKErrorPartialFailure
235 userInfo:@{CKPartialErrorsByItemIDKey: @{zoneID:zoneNotFoundError}}];
236
237 self.fetchRecordZoneChangesCompletionBlock(error);
238 return;
239 }
240
241 // Not precisely correct in the case of multiple zone fetches. However, we don't currently do that, so it'll work for now.
242 NSError* mockError = [zone popFetchChangesError];
243 if(mockError) {
244 self.fetchRecordZoneChangesCompletionBlock(mockError);
245 return;
246 }
247
248 // Extract the database at the last time they asked
249 CKServerChangeToken* token = self.optionsByRecordZoneID[zoneID].previousServerChangeToken;
250 NSMutableDictionary<CKRecordID*, CKRecord*>* lastDatabase = token ? zone.pastDatabases[token] : nil;
251
252 // You can fetch with the current change token; that's fine
253 if([token isEqual:zone.currentChangeToken]) {
254 lastDatabase = zone.currentDatabase;
255 }
256
257 ckksnotice("fakeck", zone.zoneID, "FakeCKFetchRecordZoneChangesOperation(%@): database is currently %@ change token %@ database then: %@", zone.zoneID, zone.currentDatabase, token, lastDatabase);
258
259 if(!lastDatabase && token) {
260 ckksnotice("fakeck", zone.zoneID, "no database for this change token: failing fetch with 'CKErrorChangeTokenExpired'");
261 self.fetchRecordZoneChangesCompletionBlock([[CKPrettyError alloc]
262 initWithDomain:CKErrorDomain
263 code:CKErrorPartialFailure userInfo:@{CKPartialErrorsByItemIDKey:
264 @{zoneID:[[CKPrettyError alloc] initWithDomain:CKErrorDomain code:CKErrorChangeTokenExpired userInfo:@{}]}
265 }]);
266 return;
267 }
268
269 [zone.currentDatabase enumerateKeysAndObjectsUsingBlock:^(CKRecordID * _Nonnull recordID, CKRecord * _Nonnull record, BOOL * _Nonnull stop) {
270
271 id last = [lastDatabase objectForKey: recordID];
272 if(!last || ![record isEqual:last]) {
273 self.recordChangedBlock(record);
274 }
275 }];
276
277 // iterate through lastDatabase, and delete items that aren't in database
278 [lastDatabase enumerateKeysAndObjectsUsingBlock:^(CKRecordID * _Nonnull recordID, CKRecord * _Nonnull record, BOOL * _Nonnull stop) {
279
280 id current = [zone.currentDatabase objectForKey: recordID];
281 if(current == nil) {
282 self.recordWithIDWasDeletedBlock(recordID, [record recordType]);
283 }
284 }];
285
286 self.recordZoneChangeTokensUpdatedBlock(zoneID, zone.currentChangeToken, nil);
287 self.recordZoneFetchCompletionBlock(zoneID, zone.currentChangeToken, nil, NO, nil);
288 self.fetchRecordZoneChangesCompletionBlock(nil);
289 }
290 }
291
292 +(FakeCKDatabase*) ckdb {
293 // Shouldn't ever be called: must be mocked out.
294 @throw [NSException exceptionWithName:NSInternalInconsistencyException
295 reason:[NSString stringWithFormat:@"+ckdb[] must be mocked out for use"]
296 userInfo:nil];
297 }
298 @end
299
300
301 // Do literally nothing
302 @implementation FakeAPSConnection
303 @synthesize delegate;
304
305 - (id)initWithEnvironmentName:(NSString *)environmentName namedDelegatePort:(NSString*)namedDelegatePort queue:(dispatch_queue_t)queue {
306 if(self = [super init]) {
307 }
308 return self;
309 }
310
311 - (void)setEnabledTopics:(NSArray *)enabledTopics {
312 }
313
314 @end
315
316 // Do literally nothing
317 @implementation FakeNSNotificationCenter
318 + (instancetype)defaultCenter {
319 return [[FakeNSNotificationCenter alloc] init];
320 }
321 - (void)addObserver:(id)observer selector:(SEL)aSelector name:(nullable NSNotificationName)aName object:(nullable id)anObject {
322 }
323 - (void)removeObserver:(id)observer {
324 }
325 @end
326
327 @interface FakeCKZone ()
328 @property NSMutableArray<NSError*>* fetchErrors;
329 @end
330
331 @implementation FakeCKZone
332 - (instancetype)initZone: (CKRecordZoneID*) zoneID {
333 if(self = [super init]) {
334
335 _zoneID = zoneID;
336 _currentDatabase = [[NSMutableDictionary alloc] init];
337 _pastDatabases = [[NSMutableDictionary alloc] init];
338
339 _fetchErrors = [[NSMutableArray alloc] init];
340
341 [self rollChangeToken];
342 }
343 return self;
344 }
345
346 - (void)rollChangeToken {
347 NSData* changeToken = [[[NSUUID UUID] UUIDString] dataUsingEncoding:NSUTF8StringEncoding];
348 self.currentChangeToken = [[CKServerChangeToken alloc] initWithData: changeToken];
349 }
350
351 - (void)addToZone: (CKKSCKRecordHolder*) item zoneID: (CKRecordZoneID*) zoneID {
352 CKRecord* record = [item CKRecordWithZoneID: zoneID];
353 [self addToZone: record];
354 }
355
356 - (void)addToZone: (CKRecord*) record {
357 // Save off this current databse
358 self.pastDatabases[self.currentChangeToken] = [self.currentDatabase mutableCopy];
359
360 [self rollChangeToken];
361
362 record.etag = [self.currentChangeToken description];
363 ckksnotice("fakeck", self.zoneID, "change tag: %@", record.recordChangeTag);
364 record.modificationDate = [NSDate date];
365 self.currentDatabase[record.recordID] = record;
366 }
367
368 - (NSError * _Nullable)errorFromSavingRecord:(CKRecord*) record {
369 CKRecord* existingRecord = self.currentDatabase[record.recordID];
370 if(existingRecord && ![existingRecord.recordChangeTag isEqualToString: record.recordChangeTag]) {
371 ckksnotice("fakeck", self.zoneID, "change tag mismatch! Fail the write: %@ %@", record, existingRecord);
372
373 // TODO: doesn't yet support CKRecordChangedErrorAncestorRecordKey, since I don't understand it
374 return [[CKPrettyError alloc] initWithDomain:CKErrorDomain code:CKErrorServerRecordChanged
375 userInfo:@{CKRecordChangedErrorClientRecordKey:record,
376 CKRecordChangedErrorServerRecordKey:existingRecord}];
377 }
378
379 if(!existingRecord && record.etag != nil) {
380 ckksnotice("fakeck", self.zoneID, "update to a record that doesn't exist! Fail the write: %@", record);
381 return [[CKPrettyError alloc] initWithDomain:CKErrorDomain code:CKErrorUnknownItem
382 userInfo:nil];
383 }
384 return nil;
385 }
386
387 - (void)addCKRecordToZone:(CKRecord*) record {
388 if([self errorFromSavingRecord: record]) {
389 ckksnotice("fakeck", self.zoneID, "change tag mismatch! Fail the write!");
390 }
391
392 [self addToZone: record];
393 }
394
395 - (NSError*)deleteCKRecordIDFromZone:(CKRecordID*) recordID {
396 // todo: fail somehow
397
398 self.pastDatabases[self.currentChangeToken] = [self.currentDatabase mutableCopy];
399 [self rollChangeToken];
400
401 [self.currentDatabase removeObjectForKey: recordID];
402 return nil;
403 }
404
405 - (void)failNextFetchWith: (NSError*) fetchChangesError {
406 @synchronized(self.fetchErrors) {
407 [self.fetchErrors addObject: fetchChangesError];
408 }
409 }
410
411 - (NSError * _Nullable)popFetchChangesError {
412 NSError* error = nil;
413 @synchronized(self.fetchErrors) {
414 if(self.fetchErrors.count > 0) {
415 error = self.fetchErrors[0];
416 [self.fetchErrors removeObjectAtIndex:0];
417 }
418 }
419 return error;
420 }
421 @end
422
423 @implementation FakeCKKSNotifier
424 +(void)post:(NSString*)notification {
425 if(notification) {
426 // This isn't actually fake, but XCTest likes NSNotificationCenter a whole lot.
427 // These notifications shouldn't escape this process, so it's perfect.
428 secnotice("ckks", "sending fake NSNotification %@", notification);
429 [[NSNotificationCenter defaultCenter] postNotificationName:notification object:nil];
430 }
431 }
432 @end
433
434 #endif // OCTAGON
435