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