]> git.saurik.com Git - apple/security.git/blob - keychain/ckks/CKKSHealKeyHierarchyOperation.m
Security-59754.41.1.tar.gz
[apple/security.git] / keychain / ckks / CKKSHealKeyHierarchyOperation.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 #import "CKKSKeychainView.h"
25 #import "CKKSCurrentKeyPointer.h"
26 #import "CKKSKey.h"
27 #import "CKKSHealKeyHierarchyOperation.h"
28 #import "CKKSGroupOperation.h"
29 #import "CKKSAnalytics.h"
30 #import "keychain/ckks/CloudKitCategories.h"
31 #import "keychain/ckks/CKKSHealTLKSharesOperation.h"
32 #import "keychain/categories/NSError+UsefulConstructors.h"
33 #import "keychain/ot/ObjCImprovements.h"
34
35 #if OCTAGON
36
37 @interface CKKSHealKeyHierarchyOperation ()
38 @property NSBlockOperation* cloudkitModifyOperationFinished;
39 @end
40
41 @implementation CKKSHealKeyHierarchyOperation
42 @synthesize intendedState = _intendedState;
43
44 - (instancetype)init {
45 return nil;
46 }
47
48 - (instancetype)initWithDependencies:(CKKSOperationDependencies*)dependencies
49 ckks:(CKKSKeychainView*)ckks
50 intending:(OctagonState*)intendedState
51 errorState:(OctagonState*)errorState
52 {
53 if((self = [super init])) {
54 _deps = dependencies;
55 _ckks = ckks;
56
57 _intendedState = intendedState;
58 _nextState = errorState;
59 }
60 return self;
61 }
62
63 - (void)groupStart {
64 /*
65 * We've been invoked because something is wonky with the key hierarchy.
66 *
67 * Attempt to figure out what it is, and what we can do about it.
68 *
69 * The answer "nothing, everything is terrible" is acceptable.
70 */
71
72 WEAKIFY(self);
73
74 CKKSKeychainView* ckks = self.ckks;
75 if(!ckks) {
76 ckkserror("ckksheal", ckks, "no CKKS object");
77 return;
78 }
79
80 if(self.cancelled) {
81 ckksnotice("ckksheal", ckks, "CKKSHealKeyHierarchyOperation cancelled, quitting");
82 return;
83 }
84
85 NSArray<CKKSPeerProviderState*>* currentTrustStates = self.deps.currentTrustStates;
86
87 // Synchronous, on some thread. Get back on the CKKS queue for SQL thread-safety.
88 [ckks dispatchSyncWithSQLTransaction:^CKKSDatabaseTransactionResult{
89 if(self.cancelled) {
90 ckksnotice("ckksheal", ckks, "CKKSHealKeyHierarchyOperation cancelled, quitting");
91 return CKKSDatabaseTransactionRollback;
92 }
93
94 NSError* error = nil;
95
96 CKKSCurrentKeySet* keyset = [CKKSCurrentKeySet loadForZone:ckks.zoneID];
97
98 bool changedCurrentTLK = false;
99 bool changedCurrentClassA = false;
100 bool changedCurrentClassC = false;
101
102 if(keyset.error) {
103 self.error = keyset.error;
104 ckkserror("ckksheal", ckks, "couldn't load current key set, attempting to proceed: %@", keyset.error);
105 } else {
106 ckksnotice("ckksheal", ckks, "Key set is %@", keyset);
107 }
108
109 // There's all sorts of brokenness that could exist. For now, we check for:
110 //
111 // 1. Current key pointers are nil.
112 // 2. Keys do not exist in local keychain (but TLK does)
113 // 3. Keys do not exist in local keychain (including TLK)
114 // 4. Class A or Class C keys do not wrap immediately to top TLK.
115 //
116
117 if(keyset.currentTLKPointer && keyset.currentClassAPointer && keyset.currentClassCPointer &&
118 (!keyset.tlk || !keyset.classA || !keyset.classC)) {
119 // Huh. No keys, but some current key pointers? Weird.
120 // If we haven't done one yet, initiate a refetch of everything from cloudkit, and write down that we did so
121 if(!ckks.keyStateMachineRefetched) {
122 ckksnotice("ckksheal", ckks, "Have current key pointers, but no keys. This is exceptional; requesting full refetch");
123 self.nextState = SecCKKSZoneKeyStateNeedFullRefetch;
124 return CKKSDatabaseTransactionCommit;
125 }
126 }
127
128 // No current key records. That's... odd.
129 if(!keyset.currentTLKPointer) {
130 ckksnotice("ckksheal", ckks, "No current TLK pointer?");
131 keyset.currentTLKPointer = [[CKKSCurrentKeyPointer alloc] initForClass: SecCKKSKeyClassTLK currentKeyUUID:nil zoneID:ckks.zoneID encodedCKRecord:nil];
132 }
133 if(!keyset.currentClassAPointer) {
134 ckksnotice("ckksheal", ckks, "No current ClassA pointer?");
135 keyset.currentClassAPointer = [[CKKSCurrentKeyPointer alloc] initForClass: SecCKKSKeyClassA currentKeyUUID:nil zoneID:ckks.zoneID encodedCKRecord:nil];
136 }
137 if(!keyset.currentClassCPointer) {
138 ckksnotice("ckksheal", ckks, "No current ClassC pointer?");
139 keyset.currentClassCPointer = [[CKKSCurrentKeyPointer alloc] initForClass: SecCKKSKeyClassC currentKeyUUID:nil zoneID:ckks.zoneID encodedCKRecord:nil];
140 }
141
142
143 if(keyset.currentTLKPointer.currentKeyUUID == nil || keyset.currentClassAPointer.currentKeyUUID == nil || keyset.currentClassCPointer.currentKeyUUID == nil ||
144 keyset.tlk == nil || keyset.classA == nil || keyset.classC == nil ||
145 ![keyset.classA.parentKeyUUID isEqualToString: keyset.tlk.uuid] || ![keyset.classC.parentKeyUUID isEqualToString: keyset.tlk.uuid]) {
146
147 // The records exist, but are broken. Point them at something reasonable.
148 NSArray<CKKSKey*>* keys = [CKKSKey allKeys:ckks.zoneID error:&error];
149
150 CKKSKey* newTLK = nil;
151 CKKSKey* newClassAKey = nil;
152 CKKSKey* newClassCKey = nil;
153
154 NSMutableArray<CKRecord *>* recordsToSave = [[NSMutableArray alloc] init];
155 NSMutableArray<CKRecordID *>* recordIDsToDelete = [[NSMutableArray alloc] init];
156
157 // Find the current top local key. That's our new TLK.
158 for(CKKSKey* key in keys) {
159 CKKSKey* topKey = [key topKeyInAnyState: &error];
160 if(newTLK == nil) {
161 newTLK = topKey;
162 } else if(![newTLK.uuid isEqualToString: topKey.uuid]) {
163 ckkserror("ckksheal", ckks, "key hierarchy has split: there's two top keys. Currently we don't handle this situation.");
164 self.error = [NSError errorWithDomain:CKKSErrorDomain
165 code:CKKSSplitKeyHierarchy
166 description:[NSString stringWithFormat:@"Key hierarchy has split: %@ and %@ are roots", newTLK, topKey]];
167 self.nextState = SecCKKSZoneKeyStateError;
168 return CKKSDatabaseTransactionCommit;
169 }
170 }
171
172 if(!newTLK) {
173 // We don't have any TLKs lying around, but we're supposed to heal the key hierarchy. This isn't any good; let's wait for TLK creation.
174 ckkserror("ckksheal", ckks, "No possible TLK found. Waiting for creation.");
175 self.nextState = SecCKKSZoneKeyStateWaitForTLKCreation;
176 return CKKSDatabaseTransactionCommit;
177 }
178
179 if(![newTLK validTLK:&error]) {
180 // Something has gone horribly wrong. Enter error state.
181 ckkserror("ckkskey", ckks, "CKKS claims %@ is not a valid TLK: %@", newTLK, error);
182 self.error = [NSError errorWithDomain:CKKSErrorDomain code:CKKSInvalidTLK description:@"Invalid TLK from CloudKit (during heal)" underlying:error];
183 self.nextState = SecCKKSZoneKeyStateError;
184 return CKKSDatabaseTransactionCommit;
185 }
186
187 // This key is our proposed TLK.
188 if(![newTLK tlkMaterialPresentOrRecoverableViaTLKShare:currentTrustStates
189 error:&error]) {
190 // TLK is valid, but not present locally
191 if(error && [self.deps.lockStateTracker isLockedError:error]) {
192 ckksnotice("ckkskey", ckks, "Received a TLK(%@), but keybag appears to be locked. Entering a waiting state.", newTLK);
193 self.nextState = SecCKKSZoneKeyStateWaitForUnlock;
194 } else {
195 ckksnotice("ckkskey", ckks, "Received a TLK(%@) which we don't have in the local keychain: %@", newTLK, error);
196 self.error = error;
197 self.nextState = SecCKKSZoneKeyStateTLKMissing;
198 }
199 return CKKSDatabaseTransactionCommit;
200 }
201
202 // We have our new TLK.
203 if(![keyset.currentTLKPointer.currentKeyUUID isEqualToString: newTLK.uuid]) {
204 // And it's even actually new!
205 keyset.tlk = newTLK;
206 keyset.currentTLKPointer.currentKeyUUID = newTLK.uuid;
207 changedCurrentTLK = true;
208 }
209
210 // Find some class A and class C keys directly under this one.
211 for(CKKSKey* key in keys) {
212 if([key.parentKeyUUID isEqualToString: newTLK.uuid]) {
213 if([key.keyclass isEqualToString: SecCKKSKeyClassA] &&
214 (keyset.currentClassAPointer.currentKeyUUID == nil ||
215 ![keyset.classA.parentKeyUUID isEqualToString: newTLK.uuid] ||
216 keyset.classA == nil)
217 ) {
218 keyset.classA = key;
219 keyset.currentClassAPointer.currentKeyUUID = key.uuid;
220 changedCurrentClassA = true;
221 }
222
223 if([key.keyclass isEqualToString: SecCKKSKeyClassC] &&
224 (keyset.currentClassCPointer.currentKeyUUID == nil ||
225 ![keyset.classC.parentKeyUUID isEqualToString: newTLK.uuid] ||
226 keyset.classC == nil)
227 ) {
228 keyset.classC = key;
229 keyset.currentClassCPointer.currentKeyUUID = key.uuid;
230 changedCurrentClassC = true;
231 }
232 }
233 }
234
235 if(!keyset.currentClassAPointer.currentKeyUUID) {
236 newClassAKey = [CKKSKey randomKeyWrappedByParent:newTLK error:&error];
237 [newClassAKey saveKeyMaterialToKeychain:&error];
238
239 if(error && [ckks.lockStateTracker isLockedError:error]) {
240 ckksnotice("ckksheal", ckks, "Couldn't create a new class A key, but keybag appears to be locked. Entering waitforunlock.");
241 self.error = error;
242 self.nextState = SecCKKSZoneKeyStateWaitForUnlock;
243 return CKKSDatabaseTransactionCommit;
244 } else if(error) {
245 ckkserror("ckksheal", ckks, "couldn't create new classA key: %@", error);
246 self.error = error;
247 self.nextState = SecCKKSZoneKeyStateError;
248 return CKKSDatabaseTransactionCommit;
249 }
250
251 keyset.classA = newClassAKey;
252 keyset.currentClassAPointer.currentKeyUUID = newClassAKey.uuid;
253 changedCurrentClassA = true;
254 }
255 if(!keyset.currentClassCPointer.currentKeyUUID) {
256 newClassCKey = [CKKSKey randomKeyWrappedByParent:newTLK error:&error];
257 [newClassCKey saveKeyMaterialToKeychain:&error];
258
259 if(error && [ckks.lockStateTracker isLockedError:error]) {
260 ckksnotice("ckksheal", ckks, "Couldn't create a new class C key, but keybag appears to be locked. Entering waitforunlock.");
261 self.error = error;
262 self.nextState = SecCKKSZoneKeyStateWaitForUnlock;
263 return CKKSDatabaseTransactionCommit;
264 } else if(error) {
265 ckkserror("ckksheal", ckks, "couldn't create new class C key: %@", error);
266 self.error = error;
267 self.nextState = SecCKKSZoneKeyStateError;
268 return CKKSDatabaseTransactionCommit;
269 }
270
271 keyset.classC = newClassCKey;
272 keyset.currentClassCPointer.currentKeyUUID = newClassCKey.uuid;
273 changedCurrentClassC = true;
274 }
275
276 ckksnotice("ckksheal", ckks, "Attempting to move to new key hierarchy: %@", keyset);
277
278 // Note: we never make a new TLK here. So, don't save it back to CloudKit.
279 //if(newTLK) {
280 // [recordsToSave addObject: [newTLK CKRecordWithZoneID: ckks.zoneID]];
281 //}
282 if(newClassAKey) {
283 [recordsToSave addObject: [newClassAKey CKRecordWithZoneID: ckks.zoneID]];
284 }
285 if(newClassCKey) {
286 [recordsToSave addObject: [newClassCKey CKRecordWithZoneID: ckks.zoneID]];
287 }
288
289 if(changedCurrentTLK) {
290 [recordsToSave addObject: [keyset.currentTLKPointer CKRecordWithZoneID: ckks.zoneID]];
291 }
292 if(changedCurrentClassA) {
293 [recordsToSave addObject: [keyset.currentClassAPointer CKRecordWithZoneID: ckks.zoneID]];
294 }
295 if(changedCurrentClassC) {
296 [recordsToSave addObject: [keyset.currentClassCPointer CKRecordWithZoneID: ckks.zoneID]];
297 }
298
299 // We've selected a new TLK. Compute any TLKShares that should go along with it.
300 NSSet<CKKSTLKShareRecord*>* tlkShares = [CKKSHealTLKSharesOperation createMissingKeyShares:keyset
301 trustStates:currentTrustStates
302 error:&error];
303 if(error) {
304 ckkserror("ckksshare", ckks, "Unable to create TLK shares for new tlk: %@", error);
305 return CKKSDatabaseTransactionRollback;
306 }
307
308 for(CKKSTLKShareRecord* share in tlkShares) {
309 CKRecord* record = [share CKRecordWithZoneID:ckks.zoneID];
310 [recordsToSave addObject: record];
311 }
312
313 // Kick off the CKOperation
314
315 ckksnotice("ckksheal", ckks, "Saving new records %@", recordsToSave);
316
317 // Use the spare operation trick to wait for the CKModifyRecordsOperation to complete
318 self.cloudkitModifyOperationFinished = [NSBlockOperation named:@"heal-cloudkit-modify-operation-finished" withBlock:^{}];
319 [self dependOnBeforeGroupFinished: self.cloudkitModifyOperationFinished];
320
321 CKModifyRecordsOperation* modifyRecordsOp = nil;
322
323 NSMutableDictionary<CKRecordID*, CKRecord*>* attemptedRecords = [[NSMutableDictionary alloc] init];
324 for(CKRecord* record in recordsToSave) {
325 attemptedRecords[record.recordID] = record;
326 }
327
328 // Get the CloudKit operation ready...
329 modifyRecordsOp = [[CKModifyRecordsOperation alloc] initWithRecordsToSave:recordsToSave recordIDsToDelete:recordIDsToDelete];
330 modifyRecordsOp.atomic = YES;
331 modifyRecordsOp.longLived = NO; // The keys are only in memory; mark this explicitly not long-lived
332
333 // This needs to happen for CKKS to be usable by PCS/cloudd. Make it happen.
334 modifyRecordsOp.configuration.automaticallyRetryNetworkFailures = NO;
335 modifyRecordsOp.configuration.discretionaryNetworkBehavior = CKOperationDiscretionaryNetworkBehaviorNonDiscretionary;
336 modifyRecordsOp.configuration.isCloudKitSupportOperation = YES;
337
338 modifyRecordsOp.group = self.deps.ckoperationGroup;
339 ckksnotice("ckksheal", ckks, "Operation group is %@", self.deps.ckoperationGroup);
340
341 modifyRecordsOp.perRecordCompletionBlock = ^(CKRecord *record, NSError * _Nullable error) {
342 STRONGIFY(self);
343 CKKSKeychainView* blockCKKS = self.ckks;
344
345 // These should all fail or succeed as one. Do the hard work in the records completion block.
346 if(!error) {
347 ckksnotice("ckksheal", blockCKKS, "Successfully completed upload for %@", record.recordID.recordName);
348 } else {
349 ckkserror("ckksheal", blockCKKS, "error on row: %@ %@", error, record);
350 }
351 };
352
353 modifyRecordsOp.modifyRecordsCompletionBlock = ^(NSArray<CKRecord *> *savedRecords, NSArray<CKRecordID *> *deletedRecordIDs, NSError *error) {
354 STRONGIFY(self);
355 CKKSKeychainView* strongCKKS = self.ckks;
356 if(!self) {
357 ckkserror_global("ckks", "received callback for released object");
358 return;
359 }
360
361 ckksnotice("ckksheal", strongCKKS, "Completed Key Heal CloudKit operation with error: %@", error);
362
363 [strongCKKS dispatchSyncWithSQLTransaction:^CKKSDatabaseTransactionResult{
364 if(error == nil) {
365 [[CKKSAnalytics logger] logSuccessForEvent:CKKSEventProcessHealKeyHierarchy zoneName:ckks.zoneName];
366 // Success. Persist the keys to the CKKS database.
367
368 // Save the new CKRecords to the before persisting to database
369 for(CKRecord* record in savedRecords) {
370 if([newTLK matchesCKRecord: record]) {
371 newTLK.storedCKRecord = record;
372 } else if([newClassAKey matchesCKRecord: record]) {
373 newClassAKey.storedCKRecord = record;
374 } else if([newClassCKey matchesCKRecord: record]) {
375 newClassCKey.storedCKRecord = record;
376
377 } else if([keyset.currentTLKPointer matchesCKRecord: record]) {
378 keyset.currentTLKPointer.storedCKRecord = record;
379 } else if([keyset.currentClassAPointer matchesCKRecord: record]) {
380 keyset.currentClassAPointer.storedCKRecord = record;
381 } else if([keyset.currentClassCPointer matchesCKRecord: record]) {
382 keyset.currentClassCPointer.storedCKRecord = record;
383 }
384 }
385
386 NSError* localerror = nil;
387
388 [newTLK saveToDatabaseAsOnlyCurrentKeyForClassAndState: &localerror];
389 [newClassAKey saveToDatabaseAsOnlyCurrentKeyForClassAndState: &localerror];
390 [newClassCKey saveToDatabaseAsOnlyCurrentKeyForClassAndState: &localerror];
391
392 [keyset.currentTLKPointer saveToDatabase: &localerror];
393 [keyset.currentClassAPointer saveToDatabase: &localerror];
394 [keyset.currentClassCPointer saveToDatabase: &localerror];
395
396 // save all the TLKShares, too
397 for(CKKSTLKShareRecord* share in tlkShares) {
398 [share saveToDatabase:&localerror];
399 }
400
401 if(localerror != nil) {
402 ckkserror("ckksheal", strongCKKS, "couldn't save new key hierarchy to database; this is very bad: %@", localerror);
403 self.error = localerror;
404 self.nextState = SecCKKSZoneKeyStateError;
405 return CKKSDatabaseTransactionRollback;
406 } else {
407 // Everything is groovy. HOWEVER, we might still not have processed the keys. Ask for that!
408 self.nextState = SecCKKSZoneKeyStateProcess;
409 }
410 } else {
411 // ERROR. This isn't a total-failure error state, but one that should kick off a healing process.
412 [[CKKSAnalytics logger] logUnrecoverableError:error forEvent:CKKSEventProcessHealKeyHierarchy zoneName:ckks.zoneName withAttributes:NULL];
413 ckkserror("ckksheal", strongCKKS, "couldn't save new key hierarchy to CloudKit: %@", error);
414 [strongCKKS _onqueueCKWriteFailed:error attemptedRecordsChanged:attemptedRecords];
415
416 self.nextState = SecCKKSZoneKeyStateNewTLKsFailed;
417 }
418 return CKKSDatabaseTransactionCommit;
419 }];
420
421 // Notify that we're done
422 [self.operationQueue addOperation: self.cloudkitModifyOperationFinished];
423 };
424
425 [ckks.database addOperation: modifyRecordsOp];
426 return true;
427 }
428
429 // Check if CKKS can recover this TLK.
430
431 if(![keyset.tlk validTLK:&error]) {
432 // Something has gone horribly wrong. Enter error state.
433 ckkserror("ckkskey", ckks, "CKKS claims %@ is not a valid TLK: %@", keyset.tlk, error);
434 self.error = [NSError errorWithDomain:CKKSErrorDomain code:CKKSInvalidTLK description:@"Invalid TLK from CloudKit (during heal)" underlying:error];
435 self.nextState = SecCKKSZoneKeyStateError;
436 return CKKSDatabaseTransactionCommit;
437 }
438
439 // This key is our proposed TLK.
440 if(![keyset.tlk tlkMaterialPresentOrRecoverableViaTLKShare:currentTrustStates
441 error:&error]) {
442 // TLK is valid, but not present locally
443 if(error && [self.deps.lockStateTracker isLockedError:error]) {
444 ckksnotice("ckkskey", ckks, "Received a TLK(%@), but keybag appears to be locked. Entering a waiting state.", keyset.tlk);
445 self.nextState = SecCKKSZoneKeyStateWaitForUnlock;
446 } else {
447 ckksnotice("ckkskey", ckks, "Received a TLK(%@) which we don't have in the local keychain: %@", keyset.tlk, error);
448 self.error = error;
449 self.nextState = SecCKKSZoneKeyStateTLKMissing;
450 }
451 return CKKSDatabaseTransactionCommit;
452 }
453
454 if(![self ensureKeyPresent:keyset.tlk]) {
455 return CKKSDatabaseTransactionRollback;
456 }
457
458 if(![self ensureKeyPresent:keyset.classA]) {
459 return CKKSDatabaseTransactionRollback;
460 }
461
462 if(![self ensureKeyPresent:keyset.classC]) {
463 return CKKSDatabaseTransactionRollback;
464 }
465
466 // Seems good to us. Check if we're ready?
467 self.nextState = self.intendedState;
468
469 return CKKSDatabaseTransactionCommit;
470 }];
471 }
472
473 - (bool)ensureKeyPresent:(CKKSKey*)key {
474 NSError* error = nil;
475 CKKSKeychainView* ckks = self.ckks;
476
477 [key loadKeyMaterialFromKeychain:&error];
478 if(error) {
479 ckkserror("ckksheal", ckks, "Couldn't load key(%@) from keychain. Attempting recovery: %@", key, error);
480 error = nil;
481 [key unwrapViaKeyHierarchy: &error];
482 if(error) {
483 if([ckks.lockStateTracker isLockedError:error]) {
484 ckkserror("ckksheal", ckks, "Couldn't unwrap key(%@) using key hierarchy due to the lock state: %@", key, error);
485 self.nextState = SecCKKSZoneKeyStateWaitForUnlock;
486 self.error = error;
487 return false;
488 }
489 ckkserror("ckksheal", ckks, "Couldn't unwrap key(%@) using key hierarchy. Keys are broken, quitting: %@", key, error);
490 self.error = error;
491 self.nextState = SecCKKSZoneKeyStateError;
492 self.error = error;
493 return false;
494 }
495 [key saveKeyMaterialToKeychain:&error];
496 if(error) {
497 if([ckks.lockStateTracker isLockedError:error]) {
498 ckkserror("ckksheal", ckks, "Couldn't save key(%@) to keychain due to the lock state: %@", key, error);
499 self.nextState = SecCKKSZoneKeyStateWaitForUnlock;
500 self.error = error;
501 return false;
502 }
503 ckkserror("ckksheal", ckks, "Couldn't save key(%@) to keychain: %@", key, error);
504 self.error = error;
505 self.nextState = SecCKKSZoneKeyStateError;
506 return false;
507 }
508 }
509 return true;
510 }
511
512 - (void)cancel {
513 [self.cloudkitModifyOperationFinished cancel];
514 [super cancel];
515 }
516
517 @end;
518
519 #endif