]> git.saurik.com Git - apple/security.git/blob - keychain/ckks/CKKSUpdateCurrentItemPointerOperation.m
Security-58286.251.4.tar.gz
[apple/security.git] / keychain / ckks / CKKSUpdateCurrentItemPointerOperation.m
1 /*
2 * Copyright (c) 2017 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/CKKSKeychainView.h"
27 #import "keychain/ckks/CKKSOutgoingQueueEntry.h"
28 #import "keychain/ckks/CKKSIncomingQueueEntry.h"
29 #import "keychain/ckks/CKKSCurrentItemPointer.h"
30 #import "keychain/ckks/CKKSUpdateCurrentItemPointerOperation.h"
31 #import "keychain/ckks/CKKSManifest.h"
32 #import "keychain/ckks/CloudKitCategories.h"
33 #import "keychain/categories/NSError+UsefulConstructors.h"
34
35 #include <securityd/SecItemServer.h>
36 #include <securityd/SecItemSchema.h>
37 #include <securityd/SecItemDb.h>
38 #include <Security/SecItemPriv.h>
39 #include <securityd/SecDbQuery.h>
40 #import <CloudKit/CloudKit.h>
41
42 @interface CKKSUpdateCurrentItemPointerOperation ()
43 @property (nullable) CKModifyRecordsOperation* modifyRecordsOperation;
44 @property (nullable) CKOperationGroup* ckoperationGroup;
45
46 @property (nonnull) NSString* accessGroup;
47
48 @property (nonnull) NSData* newerItemPersistentRef;
49 @property (nonnull) NSData* newerItemSHA1;
50 @property (nullable) NSData* oldItemPersistentRef;
51 @property (nullable) NSData* oldItemSHA1;
52
53 // Store these as properties, so we can release them in our -dealloc
54 @property (nullable) SecDbItemRef newItem;
55 @property (nullable) SecDbItemRef oldItem;
56 @end
57
58 @implementation CKKSUpdateCurrentItemPointerOperation
59
60 - (instancetype)initWithCKKSKeychainView:(CKKSKeychainView*)ckks
61 newItem:(NSData*)newItemPersistentRef
62 hash:(NSData*)newItemSHA1
63 accessGroup:(NSString*)accessGroup
64 identifier:(NSString*)identifier
65 replacing:(NSData* _Nullable)oldCurrentItemPersistentRef
66 hash:(NSData*)oldItemSHA1
67 ckoperationGroup:(CKOperationGroup*)ckoperationGroup
68 {
69 if((self = [super init])) {
70 _ckks = ckks;
71
72 _newerItemPersistentRef = newItemPersistentRef;
73 _newerItemSHA1 = newItemSHA1;
74 _oldItemPersistentRef = oldCurrentItemPersistentRef;
75 _oldItemSHA1 = oldItemSHA1;
76
77 _accessGroup = accessGroup;
78
79 _currentPointerIdentifier = [NSString stringWithFormat:@"%@-%@", accessGroup, identifier];
80 }
81 return self;
82 }
83
84 - (void)dealloc {
85 if(self) {
86 CFReleaseNull(self->_newItem);
87 CFReleaseNull(self->_oldItem);
88 }
89 }
90
91 - (void)groupStart {
92 CKKSKeychainView* ckks = self.ckks;
93 if(!ckks) {
94 ckkserror("ckkscurrent", ckks, "no CKKS object");
95 self.error = [NSError errorWithDomain:CKKSErrorDomain
96 code:errSecInternalError
97 description:@"no CKKS object"];
98 return;
99 }
100
101 __weak __typeof(self) weakSelf = self;
102
103 [ckks dispatchSyncWithAccountKeys:^bool {
104 if(self.cancelled) {
105 ckksnotice("ckkscurrent", ckks, "CKKSUpdateCurrentItemPointerOperation cancelled, quitting");
106 return false;
107 }
108
109 NSError* error = nil;
110 CFErrorRef cferror = NULL;
111
112 NSString* newItemUUID = nil;
113 NSString* oldCurrentItemUUID = nil;
114
115 self.newItem = [self _onqueueFindSecDbItem:self.newerItemPersistentRef accessGroup:self.accessGroup error:&error];
116 if(!self.newItem || error) {
117 ckksnotice("ckkscurrent", ckks, "Couldn't fetch new item, quitting: %@", error);
118 self.error = error;
119 return false;
120 }
121
122 // Now that we're on the db queue, ensure that the given hashes for the items match the hashes as they are now.
123 // That is, the items haven't changed since the caller knew about the item.
124 NSData* newItemComputedSHA1 = (NSData*) CFBridgingRelease(CFRetainSafe(SecDbItemGetSHA1(self.newItem, &cferror)));
125 if(!newItemComputedSHA1 || cferror ||
126 ![newItemComputedSHA1 isEqual:self.newerItemSHA1]) {
127 ckksnotice("ckkscurrent", ckks, "Hash mismatch for new item: %@ vs %@", newItemComputedSHA1, self.newerItemSHA1);
128 self.error = [NSError errorWithDomain:CKKSErrorDomain
129 code:CKKSItemChanged
130 description:@"New item has changed; hashes mismatch. Refetch and try again."
131 underlying:(NSError*)CFBridgingRelease(cferror)];
132 return false;
133 }
134
135 newItemUUID = (NSString*) CFBridgingRelease(CFRetainSafe(SecDbItemGetValue(self.newItem, &v10itemuuid, &cferror)));
136 if(!newItemUUID || cferror) {
137 ckkserror("ckkscurrent", ckks, "Error fetching UUID for new item: %@", cferror);
138 self.error = (NSError*) CFBridgingRelease(cferror);
139 return false;
140 }
141
142 // If the old item is nil, that's an indicator that the old item isn't expected to exist in the keychain anymore
143 NSData* oldCurrentItemHash = nil;
144 if(self.oldItemPersistentRef) {
145 self.oldItem = [self _onqueueFindSecDbItem:self.oldItemPersistentRef accessGroup:self.accessGroup error:&error];
146 if(!self.oldItem || error) {
147 ckksnotice("ckkscurrent", ckks, "Couldn't fetch old item, quitting: %@", error);
148 self.error = error;
149 return false;
150 }
151
152 oldCurrentItemHash = (NSData*) CFBridgingRelease(CFRetainSafe(SecDbItemGetSHA1(self.oldItem, &cferror)));
153 if(!oldCurrentItemHash || cferror ||
154 ![oldCurrentItemHash isEqual:self.oldItemSHA1]) {
155 ckksnotice("ckkscurrent", ckks, "Hash mismatch for old item: %@ vs %@", oldCurrentItemHash, self.oldItemSHA1);
156 self.error = [NSError errorWithDomain:CKKSErrorDomain
157 code:CKKSItemChanged
158 description:@"Old item has changed; hashes mismatch. Refetch and try again."
159 underlying:(NSError*)CFBridgingRelease(cferror)];
160 return false;
161 }
162
163 oldCurrentItemUUID = (NSString*) CFBridgingRelease(CFRetainSafe(SecDbItemGetValue(self.oldItem, &v10itemuuid, &cferror)));
164 if(!oldCurrentItemUUID || cferror) {
165 ckkserror("ckkscurrent", ckks, "Error fetching UUID for old item: %@", cferror);
166 self.error = (NSError*) CFBridgingRelease(cferror);
167 return false;
168 }
169 }
170
171 //////////////////////////////
172 // At this point, we've completed all the checks we need for the SecDbItems. Try to launch this boat!
173 ckksnotice("ckkscurrent", ckks, "Setting current pointer for %@ to %@ (from %@)", self.currentPointerIdentifier, newItemUUID, oldCurrentItemUUID);
174
175 // Ensure that there's no pending pointer update
176 CKKSCurrentItemPointer* cipPending = [CKKSCurrentItemPointer tryFromDatabase:self.currentPointerIdentifier state:SecCKKSProcessedStateRemote zoneID:ckks.zoneID error:&error];
177 if(cipPending) {
178 self.error = [NSError errorWithDomain:CKKSErrorDomain
179 code:CKKSRemoteItemChangePending
180 description:[NSString stringWithFormat:@"Update to current item pointer is pending."]];
181 ckkserror("ckkscurrent", ckks, "Attempt to set a new current item pointer when one exists: %@", self.error);
182 return false;
183 }
184
185 CKKSCurrentItemPointer* cip = [CKKSCurrentItemPointer tryFromDatabase:self.currentPointerIdentifier state:SecCKKSProcessedStateLocal zoneID:ckks.zoneID error:&error];
186
187 if(cip) {
188 // Ensure that the itempointer matches the old item (and the old item exists)
189 //
190 // We might be in the dangling-pointer case, where the 'fetch' API has returned the client a nil value because we
191 // have a CIP, but it points to a deleted keychain item.
192 // In that case, we shouldn't error out.
193 //
194 if(oldCurrentItemHash && ![cip.currentItemUUID isEqualToString: oldCurrentItemUUID]) {
195
196 ckksnotice("ckkscurrent", ckks, "current item pointer(%@) doesn't match user-supplied UUID (%@); rejecting change of current", cip, oldCurrentItemUUID);
197 self.error = [NSError errorWithDomain:CKKSErrorDomain
198 code:CKKSItemChanged
199 description:[NSString stringWithFormat:@"Current pointer(%@) does not match user-supplied %@, aborting", cip, oldCurrentItemUUID]];
200 return false;
201 }
202 // Cool. Since you know what you're updating, you're allowed to update!
203 cip.currentItemUUID = newItemUUID;
204
205 } else if(oldCurrentItemUUID) {
206 // Error case: the client thinks there's a current pointer, but we don't have one
207 ckksnotice("ckkscurrent", ckks, "Requested to update a current item pointer but one doesn't exist at %@; rejecting change of current", self.currentPointerIdentifier);
208 self.error = [NSError errorWithDomain:CKKSErrorDomain
209 code:CKKSItemChanged
210 description:[NSString stringWithFormat:@"Current pointer(%@) does not match given value of '%@', aborting", cip, oldCurrentItemUUID]];
211 return false;
212 } else {
213 // No current item pointer? How exciting! Let's make you a nice new one.
214 cip = [[CKKSCurrentItemPointer alloc] initForIdentifier:self.currentPointerIdentifier
215 currentItemUUID:newItemUUID
216 state:SecCKKSProcessedStateLocal
217 zoneID:ckks.zoneID
218 encodedCKRecord:nil];
219 ckksnotice("ckkscurrent", ckks, "Creating a new current item pointer: %@", cip);
220 }
221
222 // Check if either item is currently in any sync queue, and fail if so
223 NSArray* oqes = [CKKSOutgoingQueueEntry allUUIDs:ckks.zoneID error:&error];
224 NSArray* iqes = [CKKSIncomingQueueEntry allUUIDs:ckks.zoneID error:&error];
225 if([oqes containsObject:newItemUUID] || [iqes containsObject:newItemUUID]) {
226 error = [NSError errorWithDomain:CKKSErrorDomain
227 code:CKKSLocalItemChangePending
228 description:[NSString stringWithFormat:@"New item(%@) is being synced; can't set current pointer.", newItemUUID]];
229 }
230 if([oqes containsObject:oldCurrentItemUUID] || [iqes containsObject:oldCurrentItemUUID]) {
231 error = [NSError errorWithDomain:CKKSErrorDomain
232 code:CKKSLocalItemChangePending
233 description:[NSString stringWithFormat:@"Old item(%@) is being synced; can't set current pointer.", oldCurrentItemUUID]];
234 }
235
236 if(error) {
237 ckkserror("ckkscurrent", ckks, "Error attempting to update current item pointer %@: %@", self.currentPointerIdentifier, error);
238 self.error = error;
239 return false;
240 }
241
242 // Make sure the item is synced, though!
243 CKKSMirrorEntry* ckme = [CKKSMirrorEntry fromDatabase:cip.currentItemUUID zoneID:ckks.zoneID error:&error];
244 if(!ckme || error) {
245 ckkserror("ckkscurrent", ckks, "Error attempting to set a current item pointer to an item that isn't synced: %@ %@", cip, ckme);
246 error = [NSError errorWithDomain:CKKSErrorDomain
247 code:errSecItemNotFound
248 description:[NSString stringWithFormat:@"No synced item matching (%@); can't set current pointer.", cip.currentItemUUID]
249 underlying:error];
250
251 self.error = error;
252 return false;
253 }
254
255 if ([CKKSManifest shouldSyncManifests]) {
256 [ckks.egoManifest setCurrentItemUUID:newItemUUID forIdentifier:self.currentPointerIdentifier];
257 }
258
259 ckksnotice("ckkscurrent", ckks, "Saving new current item pointer %@", cip);
260
261 NSMutableDictionary<CKRecordID*, CKRecord*>* recordsToSave = [[NSMutableDictionary alloc] init];
262 CKRecord* record = [cip CKRecordWithZoneID:ckks.zoneID];
263 recordsToSave[record.recordID] = record;
264
265 if([CKKSManifest shouldSyncManifests]) {
266 for(CKRecord* record in [ckks.egoManifest allCKRecordsWithZoneID:ckks.zoneID]) {
267 recordsToSave[record.recordID] = record;
268 }
269 }
270
271 // Start a CKModifyRecordsOperation to save this new/updated record.
272 NSBlockOperation* modifyComplete = [[NSBlockOperation alloc] init];
273 modifyComplete.name = @"updateCurrentItemPointer-modifyRecordsComplete";
274 [self dependOnBeforeGroupFinished: modifyComplete];
275
276 self.modifyRecordsOperation = [[CKModifyRecordsOperation alloc] initWithRecordsToSave:recordsToSave.allValues recordIDsToDelete:nil];
277 self.modifyRecordsOperation.atomic = TRUE;
278 // We're likely rolling a PCS identity, or creating a new one. User cares.
279 self.modifyRecordsOperation.configuration.automaticallyRetryNetworkFailures = NO;
280 self.modifyRecordsOperation.configuration.discretionaryNetworkBehavior = CKOperationDiscretionaryNetworkBehaviorNonDiscretionary;
281
282 self.modifyRecordsOperation.savePolicy = CKRecordSaveIfServerRecordUnchanged;
283 self.modifyRecordsOperation.group = self.ckoperationGroup;
284
285 self.modifyRecordsOperation.perRecordCompletionBlock = ^(CKRecord *record, NSError * _Nullable error) {
286 __strong __typeof(weakSelf) strongSelf = weakSelf;
287 __strong __typeof(strongSelf.ckks) blockCKKS = strongSelf.ckks;
288
289 if(!error) {
290 ckksnotice("ckkscurrent", blockCKKS, "Current pointer upload successful for %@: %@", record.recordID.recordName, record);
291 } else {
292 ckkserror("ckkscurrent", blockCKKS, "error on row: %@ %@", error, record);
293 }
294 };
295
296 self.modifyRecordsOperation.modifyRecordsCompletionBlock = ^(NSArray<CKRecord *> *savedRecords, NSArray<CKRecordID *> *deletedRecordIDs, NSError *ckerror) {
297 __strong __typeof(weakSelf) strongSelf = weakSelf;
298 __strong __typeof(strongSelf.ckks) strongCKKS = strongSelf.ckks;
299 if(!strongSelf || !strongCKKS) {
300 ckkserror("ckkscurrent", strongCKKS, "received callback for released object");
301 strongSelf.error = [NSError errorWithDomain:CKKSErrorDomain
302 code:errSecInternalError
303 description:@"no CKKS object"];
304 [strongCKKS scheduleOperation: modifyComplete];
305 return;
306 }
307
308 if(ckerror) {
309 ckkserror("ckkscurrent", strongCKKS, "CloudKit returned an error: %@", ckerror);
310 strongSelf.error = ckerror;
311
312 [strongCKKS dispatchSync:^bool {
313 return [strongCKKS _onqueueCKWriteFailed:ckerror attemptedRecordsChanged:recordsToSave];
314 }];
315
316 [strongCKKS scheduleOperation: modifyComplete];
317 return;
318 }
319
320 __block NSError* error = nil;
321
322 [strongCKKS dispatchSync: ^bool{
323 for(CKRecord* record in savedRecords) {
324 // Save the item records
325 if([record.recordType isEqualToString: SecCKRecordCurrentItemType]) {
326 if([cip matchesCKRecord: record]) {
327 cip.storedCKRecord = record;
328 [cip saveToDatabase:&error];
329 if(error) {
330 ckkserror("ckkscurrent", strongCKKS, "Couldn't save new current pointer to database: %@", error);
331 }
332 } else {
333 ckkserror("ckkscurrent", strongCKKS, "CloudKit record does not match saved record, ignoring: %@ %@", record, cip);
334 }
335 }
336 else if ([CKKSManifest shouldSyncManifests] && [record.recordType isEqualToString:SecCKRecordManifestType]) {
337 CKKSManifest* manifest = [[CKKSManifest alloc] initWithCKRecord:record];
338 [manifest saveToDatabase:&error];
339 if (error) {
340 ckkserror("ckkscurrent", strongCKKS, "Couldn't save %@ to manifest: %@", record.recordID.recordName, error);
341 strongSelf.error = error;
342 }
343 }
344
345 // Schedule a 'view changed' notification
346 [strongCKKS.notifyViewChangedScheduler trigger];
347 }
348 return true;
349 }];
350
351 strongSelf.error = error;
352 [strongCKKS scheduleOperation: modifyComplete];
353 };
354
355 [self dependOnBeforeGroupFinished: self.modifyRecordsOperation];
356 [ckks.database addOperation: self.modifyRecordsOperation];
357
358 return true;
359 }];
360 }
361
362 - (SecDbItemRef _Nullable)_onqueueFindSecDbItem:(NSData*)persistentRef accessGroup:(NSString*)accessGroup error:(NSError**)error {
363 __block SecDbItemRef blockItem = NULL;
364 CFErrorRef cferror = NULL;
365 __block NSError* localerror = NULL;
366
367 CKKSKeychainView* ckks = self.ckks;
368 bool ok = kc_with_dbt(true, &cferror, ^bool (SecDbConnectionRef dbt) {
369 // Find the items from their persistent refs.
370 CFErrorRef blockcfError = NULL;
371 Query *q = query_create_with_limit( (__bridge CFDictionaryRef) @{
372 (__bridge NSString *)kSecValuePersistentRef : persistentRef,
373 (__bridge NSString *)kSecAttrAccessGroup : accessGroup,
374 },
375 NULL,
376 1,
377 &blockcfError);
378 if(blockcfError || !q) {
379 ckkserror("ckkscurrent", ckks, "couldn't create query for item persistentRef: %@", blockcfError);
380 localerror = [NSError errorWithDomain:CKKSErrorDomain
381 code:errSecParam
382 description:@"couldn't create query for new item pref"
383 underlying:(NSError*)CFBridgingRelease(blockcfError)];
384 return false;
385 }
386
387 if(!SecDbItemQuery(q, NULL, dbt, &blockcfError, ^(SecDbItemRef item, bool *stop) {
388 blockItem = CFRetainSafe(item);
389 })) {
390 query_destroy(q, NULL);
391 ckkserror("ckkscurrent", ckks, "couldn't run query for item pref: %@", blockcfError);
392 localerror = [NSError errorWithDomain:CKKSErrorDomain
393 code:errSecParam
394 description:@"couldn't run query for new item pref"
395 underlying:(NSError*)CFBridgingRelease(blockcfError)];
396 return false;
397 }
398
399 if(!query_destroy(q, &blockcfError)) {
400 ckkserror("ckkscurrent", ckks, "couldn't destroy query for item pref: %@", blockcfError);
401 localerror = [NSError errorWithDomain:CKKSErrorDomain
402 code:errSecParam
403 description:@"couldn't destroy query for item pref"
404 underlying:(NSError*)CFBridgingRelease(blockcfError)];
405 return false;
406 }
407 return true;
408 });
409
410 if(!ok || localerror) {
411 if(localerror) {
412 ckkserror("ckkscurrent", ckks, "Query failed: %@", localerror);
413 if(error) {
414 *error = localerror;
415 }
416 } else {
417 ckkserror("ckkscurrent", ckks, "Query failed, cferror is %@", cferror);
418 localerror = [NSError errorWithDomain:CKKSErrorDomain
419 code:errSecParam
420 description:@"couldn't run query"
421 underlying:(NSError*)CFBridgingRelease(cferror)];
422 if(*error) {
423 *error = localerror;
424 }
425 }
426
427 CFReleaseSafe(cferror);
428 return false;
429 }
430
431 CFReleaseSafe(cferror);
432 return blockItem;
433 }
434
435 @end
436
437 #endif // OCTAGON