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