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