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