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