]> git.saurik.com Git - apple/security.git/blob - keychain/ckks/CKKSUpdateCurrentItemPointerOperation.m
Security-58286.31.2.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 #import <CloudKit/CloudKit.h>
35
36 @interface CKKSUpdateCurrentItemPointerOperation ()
37 @property CKModifyRecordsOperation* modifyRecordsOperation;
38 @property CKOperationGroup* ckoperationGroup;
39
40 @property NSString* currentPointerIdentifier;
41 @property NSString* oldCurrentItemUUID;
42 @property NSData* oldCurrentItemHash;
43 @property NSString* currentItemUUID;
44 @end
45
46 @implementation CKKSUpdateCurrentItemPointerOperation
47
48 - (instancetype)initWithCKKSKeychainView:(CKKSKeychainView*) ckks
49 currentPointer:(NSString*)identifier
50 oldItemUUID:(NSString*)oldItemUUID
51 oldItemHash:(NSData*)oldItemHash
52 newItemUUID:(NSString*)newItemUUID
53 ckoperationGroup:(CKOperationGroup*)ckoperationGroup
54 {
55 if((self = [super init])) {
56 _ckks = ckks;
57
58 _currentPointerIdentifier = identifier;
59 _oldCurrentItemUUID = oldItemUUID;
60 _oldCurrentItemHash = oldItemHash;
61 _currentItemUUID = newItemUUID;
62 _ckoperationGroup = ckoperationGroup;
63 }
64 return self;
65 }
66
67 - (void)groupStart {
68 CKKSKeychainView* ckks = self.ckks;
69 if(!ckks) {
70 ckkserror("ckkscurrent", ckks, "no CKKS object");
71 self.error = [NSError errorWithDomain:CKKSErrorDomain
72 code:errSecInternalError
73 description:@"no CKKS object"];
74 return;
75 }
76
77 __weak __typeof(self) weakSelf = self;
78
79 [ckks dispatchSyncWithAccountKeys:^bool {
80 if(self.cancelled) {
81 ckksnotice("ckksscan", ckks, "CKKSUpdateCurrentItemPointerOperation cancelled, quitting");
82 return false;
83 }
84
85 NSError* error = nil;
86
87 // Ensure that there's no pending pointer update
88 CKKSCurrentItemPointer* cipPending = [CKKSCurrentItemPointer tryFromDatabase:self.currentPointerIdentifier state:SecCKKSProcessedStateRemote zoneID:ckks.zoneID error:&error];
89 if(cipPending) {
90 self.error = [NSError errorWithDomain:CKKSErrorDomain
91 code:CKKSRemoteItemChangePending
92 description:[NSString stringWithFormat:@"Update to current item pointer is pending."]];
93 ckkserror("ckkscurrent", ckks, "Attempt to set a new current item pointer when one exists: %@", self.error);
94 return false;
95 }
96
97 CKKSCurrentItemPointer* cip = [CKKSCurrentItemPointer tryFromDatabase:self.currentPointerIdentifier state:SecCKKSProcessedStateLocal zoneID:ckks.zoneID error:&error];
98
99 if(cip) {
100 // Ensure that the itempointer matches the old item (and the old item exists)
101 //
102 // We might be in the dangling-pointer case, where the 'fetch' API has returned the client a nil value because we
103 // have a CIP, but it points to a deleted keychain item.
104 // In that case, we shouldn't error out.
105 //
106 if(self.oldCurrentItemHash && ![cip.currentItemUUID isEqualToString: self.oldCurrentItemUUID]) {
107
108 ckksnotice("ckkscurrent", ckks, "Caller's idea of the current item pointer %@ doesn't match (%@); rejecting change of current", cip, self.oldCurrentItemUUID);
109 self.error = [NSError errorWithDomain:CKKSErrorDomain
110 code:CKKSItemChanged
111 description:[NSString stringWithFormat:@"Current pointer(%@) does not match user-supplied %@, aborting", cip, self.oldCurrentItemUUID]];
112 return false;
113 }
114 // Cool. Since you know what you're updating, you're allowed to update!
115 cip.currentItemUUID = self.currentItemUUID;
116
117 } else if(self.oldCurrentItemUUID) {
118 // Error case: the client thinks there's a current pointer, but we don't have one
119 ckksnotice("ckkscurrent", ckks, "Requested to update a current item pointer but one doesn't exist at %@; rejecting change of current", self.currentPointerIdentifier);
120 self.error = [NSError errorWithDomain:CKKSErrorDomain
121 code:CKKSItemChanged
122 description:[NSString stringWithFormat:@"Current pointer(%@) does not match given value of '%@', aborting", cip, self.oldCurrentItemUUID]];
123 return false;
124 } else {
125 // No current item pointer? How exciting! Let's make you a nice new one.
126 cip = [[CKKSCurrentItemPointer alloc] initForIdentifier:self.currentPointerIdentifier currentItemUUID:self.currentItemUUID state:SecCKKSProcessedStateLocal zoneID:ckks.zoneID encodedCKRecord:nil];
127 ckksnotice("ckkscurrent", ckks, "Creating a new current item pointer: %@", cip);
128 }
129
130 // Check if either item is currently in any sync queue, and fail if so
131 NSArray* oqes = [CKKSOutgoingQueueEntry allUUIDs:&error];
132 NSArray* iqes = [CKKSIncomingQueueEntry allUUIDs:&error];
133 if([oqes containsObject:self.currentItemUUID] || [iqes containsObject:self.currentItemUUID]) {
134 error = [NSError errorWithDomain:CKKSErrorDomain
135 code:CKKSLocalItemChangePending
136 description:[NSString stringWithFormat:@"New item(%@) is being synced; can't set current pointer.", self.currentItemUUID]];
137 }
138 if([oqes containsObject: self.oldCurrentItemUUID] || [iqes containsObject:self.oldCurrentItemUUID]) {
139 error = [NSError errorWithDomain:CKKSErrorDomain
140 code:CKKSLocalItemChangePending
141 description:[NSString stringWithFormat:@"Old item(%@) is being synced; can't set current pointer.", self.oldCurrentItemUUID]];
142 }
143
144 if(error) {
145 ckkserror("ckkscurrent", ckks, "Error attempting to update current item pointer %@: %@", self.currentPointerIdentifier, error);
146 self.error = error;
147 return false;
148 }
149
150 // Make sure the item is synced, though!
151 CKKSMirrorEntry* ckme = [CKKSMirrorEntry fromDatabase:cip.currentItemUUID zoneID:ckks.zoneID error:&error];
152 if(!ckme || error) {
153 ckkserror("ckkscurrent", ckks, "Error attempting to set a current item pointer to an item that isn't synced: %@ %@", cip, ckme);
154 error = [NSError errorWithDomain:CKKSErrorDomain
155 code:errSecItemNotFound
156 description:[NSString stringWithFormat:@"No synced item matching (%@); can't set current pointer.", cip.currentItemUUID]
157 underlying:error];
158
159 self.error = error;
160 return false;
161 }
162
163 if ([CKKSManifest shouldSyncManifests]) {
164 [ckks.egoManifest setCurrentItemUUID:self.currentItemUUID forIdentifier:self.currentPointerIdentifier];
165 }
166
167 ckksnotice("ckkscurrent", ckks, "Saving new current item pointer %@", cip);
168
169 NSMutableDictionary<CKRecordID*, CKRecord*>* recordsToSave = [[NSMutableDictionary alloc] init];
170 CKRecord* record = [cip CKRecordWithZoneID:ckks.zoneID];
171 recordsToSave[record.recordID] = record;
172
173 if([CKKSManifest shouldSyncManifests]) {
174 for(CKRecord* record in [ckks.egoManifest allCKRecordsWithZoneID:ckks.zoneID]) {
175 recordsToSave[record.recordID] = record;
176 }
177 }
178
179 // Start a CKModifyRecordsOperation to save this new/updated record.
180 NSBlockOperation* modifyComplete = [[NSBlockOperation alloc] init];
181 modifyComplete.name = @"updateCurrentItemPointer-modifyRecordsComplete";
182 [self dependOnBeforeGroupFinished: modifyComplete];
183
184 self.modifyRecordsOperation = [[CKModifyRecordsOperation alloc] initWithRecordsToSave:recordsToSave.allValues recordIDsToDelete:nil];
185 self.modifyRecordsOperation.atomic = TRUE;
186 self.modifyRecordsOperation.timeoutIntervalForRequest = 2;
187 self.modifyRecordsOperation.qualityOfService = NSQualityOfServiceUtility;
188 self.modifyRecordsOperation.savePolicy = CKRecordSaveIfServerRecordUnchanged;
189 self.modifyRecordsOperation.group = self.ckoperationGroup;
190
191 self.modifyRecordsOperation.perRecordCompletionBlock = ^(CKRecord *record, NSError * _Nullable error) {
192 __strong __typeof(weakSelf) strongSelf = weakSelf;
193 __strong __typeof(strongSelf.ckks) blockCKKS = strongSelf.ckks;
194
195 if(!error) {
196 ckksnotice("ckkscurrent", blockCKKS, "Current pointer upload successful for %@: %@", record.recordID.recordName, record);
197 } else {
198 ckkserror("ckkscurrent", blockCKKS, "error on row: %@ %@", error, record);
199 }
200 };
201
202 self.modifyRecordsOperation.modifyRecordsCompletionBlock = ^(NSArray<CKRecord *> *savedRecords, NSArray<CKRecordID *> *deletedRecordIDs, NSError *ckerror) {
203 __strong __typeof(weakSelf) strongSelf = weakSelf;
204 __strong __typeof(strongSelf.ckks) strongCKKS = strongSelf.ckks;
205 if(!strongSelf || !strongCKKS) {
206 ckkserror("ckkscurrent", strongCKKS, "received callback for released object");
207 strongSelf.error = [NSError errorWithDomain:CKKSErrorDomain
208 code:errSecInternalError
209 description:@"no CKKS object"];
210 [strongCKKS scheduleOperation: modifyComplete];
211 return;
212 }
213
214 if(ckerror) {
215 ckkserror("ckkscurrent", strongCKKS, "CloudKit returned an error: %@", ckerror);
216 strongSelf.error = ckerror;
217
218 [ckks dispatchSync:^bool {
219 return [ckks _onqueueCKWriteFailed:ckerror attemptedRecordsChanged:recordsToSave];
220 }];
221
222 [strongCKKS scheduleOperation: modifyComplete];
223 return;
224 }
225
226 __block NSError* error = nil;
227
228 [strongCKKS dispatchSync: ^bool{
229 for(CKRecord* record in savedRecords) {
230 // Save the item records
231 if([record.recordType isEqualToString: SecCKRecordCurrentItemType]) {
232 if([cip matchesCKRecord: record]) {
233 cip.storedCKRecord = record;
234 [cip saveToDatabase:&error];
235 if(error) {
236 ckkserror("ckkscurrent", strongCKKS, "Couldn't save new current pointer to database: %@", error);
237 }
238 } else {
239 ckkserror("ckkscurrent", strongCKKS, "CloudKit record does not match saved record, ignoring: %@ %@", record, cip);
240 }
241 }
242 else if ([CKKSManifest shouldSyncManifests] && [record.recordType isEqualToString:SecCKRecordManifestType]) {
243 CKKSManifest* manifest = [[CKKSManifest alloc] initWithCKRecord:record];
244 [manifest saveToDatabase:&error];
245 if (error) {
246 ckkserror("ckkscurrent", strongCKKS, "Couldn't save %@ to manifest: %@", record.recordID.recordName, error);
247 strongSelf.error = error;
248 }
249 }
250
251 // Schedule a 'view changed' notification
252 [strongCKKS.notifyViewChangedScheduler trigger];
253 }
254 return true;
255 }];
256
257 strongSelf.error = error;
258 [strongCKKS scheduleOperation: modifyComplete];
259 };
260
261 [self dependOnBeforeGroupFinished: self.modifyRecordsOperation];
262 [ckks.database addOperation: self.modifyRecordsOperation];
263
264 return true;
265 }];
266 }
267
268 @end
269
270 #endif // OCTAGON