2 * Copyright (c) 2017 Apple Inc. All Rights Reserved.
4 * @APPLE_LICENSE_HEADER_START@
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
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.
21 * @APPLE_LICENSE_HEADER_END@
24 #import "CKKSKeychainView.h"
25 #import "CKKSCurrentKeyPointer.h"
27 #import "CKKSHealKeyHierarchyOperation.h"
28 #import "CKKSGroupOperation.h"
32 @interface CKKSHealKeyHierarchyOperation ()
33 @property NSBlockOperation* cloudkitModifyOperationFinished;
34 @property CKOperationGroup* ckoperationGroup;
37 @implementation CKKSHealKeyHierarchyOperation
39 - (instancetype)init {
42 - (instancetype)initWithCKKSKeychainView:(CKKSKeychainView*)ckks ckoperationGroup:(CKOperationGroup*)ckoperationGroup {
43 if(self = [super init]) {
45 _ckoperationGroup = ckoperationGroup;
52 * We've been invoked because something is wonky with the key hierarchy.
54 * Attempt to figure out what it is, and what we can do about it.
56 * The answer "nothing, everything is terrible" is acceptable.
59 __weak __typeof(self) weakSelf = self;
61 CKKSKeychainView* ckks = self.ckks;
63 ckkserror("ckksheal", ckks, "no CKKS object");
68 ckksnotice("ckksheal", ckks, "CKKSHealKeyHierarchyOperation cancelled, quitting");
72 // Synchronous, on some thread. Get back on the CKKS queue for SQL thread-safety.
73 [ckks dispatchSync: ^bool{
75 ckksnotice("ckksheal", ckks, "CKKSHealKeyHierarchyOperation cancelled, quitting");
81 CKKSCurrentKeySet* keyset = [[CKKSCurrentKeySet alloc] initForZone:ckks.zoneID];
83 bool changedCurrentTLK = false;
84 bool changedCurrentClassA = false;
85 bool changedCurrentClassC = false;
88 self.error = keyset.error;
89 ckkserror("ckksheal", ckks, "couldn't load current key set, attempting to proceed: %@", keyset.error);
91 ckksnotice("ckksheal", ckks, "Key set is %@", keyset);
94 // There's all sorts of brokenness that could exist. For now, we check for:
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)
101 if(keyset.currentTLKPointer && keyset.currentClassAPointer && keyset.currentClassCPointer &&
102 (!keyset.tlk || !keyset.classA || !keyset.classC)) {
103 // Huh. No keys, but some current key pointers? Weird.
104 // If we haven't done one yet, initiate a refetch of everything from cloudkit, and write down that we did so
105 if(!ckks.keyStateMachineRefetched) {
106 ckksnotice("ckksheal", ckks, "Have current key pointers, but no keys. This is exceptional; requesting full refetch");
107 [ckks _onqueueAdvanceKeyStateMachineToState:SecCKKSZoneKeyStateNeedFullRefetch withError:nil];
112 // No current key records. That's... odd.
113 if(!keyset.currentTLKPointer) {
114 ckksnotice("ckksheal", ckks, "No current TLK pointer?");
115 keyset.currentTLKPointer = [[CKKSCurrentKeyPointer alloc] initForClass: SecCKKSKeyClassTLK currentKeyUUID:nil zoneID:ckks.zoneID encodedCKRecord:nil];
117 if(!keyset.currentClassAPointer) {
118 ckksnotice("ckksheal", ckks, "No current ClassA pointer?");
119 keyset.currentClassAPointer = [[CKKSCurrentKeyPointer alloc] initForClass: SecCKKSKeyClassA currentKeyUUID:nil zoneID:ckks.zoneID encodedCKRecord:nil];
121 if(!keyset.currentClassCPointer) {
122 ckksnotice("ckksheal", ckks, "No current ClassC pointer?");
123 keyset.currentClassCPointer = [[CKKSCurrentKeyPointer alloc] initForClass: SecCKKSKeyClassC currentKeyUUID:nil zoneID:ckks.zoneID encodedCKRecord:nil];
127 if(keyset.currentTLKPointer.currentKeyUUID == nil || keyset.currentClassAPointer.currentKeyUUID == nil || keyset.currentClassCPointer.currentKeyUUID == nil ||
128 keyset.tlk == nil || keyset.classA == nil || keyset.classC == nil) {
130 // The records exist, but are broken. Point them at something reasonable.
131 NSArray<CKKSKey*>* keys = [CKKSKey allKeys:ckks.zoneID error:&error];
133 CKKSKey* newTLK = nil;
134 CKKSKey* newClassAKey = nil;
135 CKKSKey* newClassCKey = nil;
137 NSMutableArray<CKRecord *>* recordsToSave = [[NSMutableArray alloc] init];
138 NSMutableArray<CKRecordID *>* recordIDsToDelete = [[NSMutableArray alloc] init];
140 // Find the current top local key. That's our new TLK.
141 for(CKKSKey* key in keys) {
142 CKKSKey* topKey = [key topKeyInAnyState: &error];
145 } else if(![newTLK.uuid isEqualToString: topKey.uuid]) {
146 ckkserror("ckksheal", ckks, "key hierarchy has split: there's two top keys. Currently we don't handle this situation.");
147 [ckks _onqueueAdvanceKeyStateMachineToState: SecCKKSZoneKeyStateError withError: [NSError errorWithDomain:@"securityd"
149 userInfo:@{NSLocalizedDescriptionKey:
150 [NSString stringWithFormat:@"Key hierarchy has split: %@ and %@ are roots", newTLK, topKey]}]];
155 if(![ckks checkTLK: newTLK error: &error]) {
156 // Was this error "I've never seen that TLK before in my life"? If so, enter the "wait for TLK sync" state.
157 if(error && [error.domain isEqualToString: @"securityd"] && error.code == errSecItemNotFound) {
158 ckksnotice("ckksheal", ckks, "Received a TLK which we don't have in the local keychain(%@). Entering waitfortlk.", newTLK);
159 [ckks _onqueueAdvanceKeyStateMachineToState:SecCKKSZoneKeyStateWaitForTLK withError:nil];
161 } else if(error && [ckks.lockStateTracker isLockedError:error]) {
162 ckksnotice("ckkskey", ckks, "Received a TLK(%@), but keybag appears to be locked. Entering WaitForUnlock.", newTLK);
163 [ckks _onqueueAdvanceKeyStateMachineToState:SecCKKSZoneKeyStateWaitForUnlock withError:nil];
167 // Otherwise, something has gone horribly wrong. enter error state.
168 ckkserror("ckksheal", ckks, "CKKS claims %@ is not a valid TLK: %@", newTLK, error);
169 NSError* newError = nil;
171 newError = [NSError errorWithDomain:@"securityd"
173 userInfo:@{NSLocalizedDescriptionKey: @"invalid TLK from CloudKit", NSUnderlyingErrorKey: error}];
175 newError = [NSError errorWithDomain:@"securityd"
177 userInfo:@{NSLocalizedDescriptionKey: @"invalid TLK from CloudKit"}];
179 [ckks _onqueueAdvanceKeyStateMachineToState:SecCKKSZoneKeyStateError withError:newError];
184 // We have our new TLK.
185 keyset.currentTLKPointer.currentKeyUUID = newTLK.uuid;
186 changedCurrentTLK = true;
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((keyset.currentClassAPointer.currentKeyUUID == nil || keyset.classA == nil) &&
192 [key.keyclass isEqualToString: SecCKKSKeyClassA]) {
193 keyset.currentClassAPointer.currentKeyUUID = key.uuid;
194 changedCurrentClassA = true;
197 if((keyset.currentClassCPointer.currentKeyUUID == nil || keyset.classC == nil) &&
198 [key.keyclass isEqualToString: SecCKKSKeyClassC]) {
199 keyset.currentClassCPointer.currentKeyUUID = key.uuid;
200 changedCurrentClassC = true;
205 if(!keyset.currentClassAPointer.currentKeyUUID) {
206 newClassAKey = [CKKSKey randomKeyWrappedByParent:newTLK error:&error];
207 [newClassAKey saveKeyMaterialToKeychain:&error];
210 ckkserror("ckksheal", ckks, "couldn't create new classA key: %@", error);
211 [ckks _onqueueAdvanceKeyStateMachineToState:SecCKKSZoneKeyStateError withError:[NSError errorWithDomain: @"securityd" code:0 userInfo:@{NSLocalizedDescriptionKey: @"couldn't create new classA key", NSUnderlyingErrorKey: error}]];
214 keyset.currentClassAPointer.currentKeyUUID = newClassAKey.uuid;
215 changedCurrentClassA = true;
217 if(!keyset.currentClassCPointer.currentKeyUUID) {
218 newClassCKey = [CKKSKey randomKeyWrappedByParent:newTLK error:&error];
219 [newClassCKey saveKeyMaterialToKeychain:&error];
222 ckkserror("ckksheal", ckks, "couldn't create new classC key: %@", error);
223 [ckks _onqueueAdvanceKeyStateMachineToState:SecCKKSZoneKeyStateError withError:[NSError errorWithDomain: @"securityd" code:0 userInfo:@{NSLocalizedDescriptionKey: @"couldn't create new classC key", NSUnderlyingErrorKey: error}]];
226 keyset.currentClassCPointer.currentKeyUUID = newClassCKey.uuid;
227 changedCurrentClassC = true;
230 // Note: we never make a new TLK here. So, don't save it back to CloudKit.
232 // [recordsToSave addObject: [newTLK CKRecordWithZoneID: ckks.zoneID]];
235 [recordsToSave addObject: [newClassAKey CKRecordWithZoneID: ckks.zoneID]];
238 [recordsToSave addObject: [newClassCKey CKRecordWithZoneID: ckks.zoneID]];
241 if(changedCurrentTLK) {
242 [recordsToSave addObject: [keyset.currentTLKPointer CKRecordWithZoneID: ckks.zoneID]];
244 if(changedCurrentClassA) {
245 [recordsToSave addObject: [keyset.currentClassAPointer CKRecordWithZoneID: ckks.zoneID]];
247 if(changedCurrentClassC) {
248 [recordsToSave addObject: [keyset.currentClassCPointer CKRecordWithZoneID: ckks.zoneID]];
251 ckksinfo("ckksheal", ckks, "Saving new keys %@ to cloudkit %@", recordsToSave, ckks.database);
253 // Use the spare operation trick to wait for the CKModifyRecordsOperation to complete
254 self.cloudkitModifyOperationFinished = [NSBlockOperation named:@"heal-cloudkit-modify-operation-finished" withBlock:^{}];
255 [self dependOnBeforeGroupFinished: self.cloudkitModifyOperationFinished];
257 CKModifyRecordsOperation* modifyRecordsOp = nil;
259 // Get the CloudKit operation ready...
260 modifyRecordsOp = [[CKModifyRecordsOperation alloc] initWithRecordsToSave:recordsToSave recordIDsToDelete:recordIDsToDelete];
261 modifyRecordsOp.atomic = YES;
262 modifyRecordsOp.longLived = NO; // The keys are only in memory; mark this explicitly not long-lived
263 modifyRecordsOp.timeoutIntervalForRequest = 2;
264 modifyRecordsOp.qualityOfService = NSQualityOfServiceUtility; // relatively important. Use Utility.
265 modifyRecordsOp.group = self.ckoperationGroup;
266 ckksnotice("ckksheal", ckks, "Operation group is %@", self.ckoperationGroup);
268 modifyRecordsOp.perRecordCompletionBlock = ^(CKRecord *record, NSError * _Nullable error) {
269 __strong __typeof(weakSelf) strongSelf = weakSelf;
270 __strong __typeof(strongSelf.ckks) blockCKKS = strongSelf.ckks;
272 // These should all fail or succeed as one. Do the hard work in the records completion block.
274 ckksnotice("ckksheal", blockCKKS, "Successfully completed upload for %@", record.recordID.recordName);
276 ckkserror("ckksheal", blockCKKS, "error on row: %@ %@", error, record);
280 modifyRecordsOp.modifyRecordsCompletionBlock = ^(NSArray<CKRecord *> *savedRecords, NSArray<CKRecordID *> *deletedRecordIDs, NSError *error) {
281 __strong __typeof(weakSelf) strongSelf = weakSelf;
282 __strong __typeof(strongSelf.ckks) strongCKKS = strongSelf.ckks;
284 secerror("ckks: received callback for released object");
288 ckksnotice("ckksheal", strongCKKS, "Completed Key Heal CloudKit operation with error: %@", error);
290 [strongCKKS dispatchSync: ^bool{
292 // Success. Persist the keys to the CKKS database.
294 // Save the new CKRecords to the before persisting to database
295 for(CKRecord* record in savedRecords) {
296 if([newTLK matchesCKRecord: record]) {
297 newTLK.storedCKRecord = record;
298 } else if([newClassAKey matchesCKRecord: record]) {
299 newClassAKey.storedCKRecord = record;
300 } else if([newClassCKey matchesCKRecord: record]) {
301 newClassCKey.storedCKRecord = record;
303 } else if([keyset.currentTLKPointer matchesCKRecord: record]) {
304 keyset.currentTLKPointer.storedCKRecord = record;
305 } else if([keyset.currentClassAPointer matchesCKRecord: record]) {
306 keyset.currentClassAPointer.storedCKRecord = record;
307 } else if([keyset.currentClassCPointer matchesCKRecord: record]) {
308 keyset.currentClassCPointer.storedCKRecord = record;
312 NSError* localerror = nil;
314 [newTLK saveToDatabaseAsOnlyCurrentKeyForClassAndState: &localerror];
315 [newClassAKey saveToDatabaseAsOnlyCurrentKeyForClassAndState: &localerror];
316 [newClassCKey saveToDatabaseAsOnlyCurrentKeyForClassAndState: &localerror];
318 [keyset.currentTLKPointer saveToDatabase: &localerror];
319 [keyset.currentClassAPointer saveToDatabase: &localerror];
320 [keyset.currentClassCPointer saveToDatabase: &localerror];
322 if(localerror != nil) {
323 ckkserror("ckksheal", strongCKKS, "couldn't save new key hierarchy to database; this is very bad: %@", localerror);
324 [strongCKKS _onqueueAdvanceKeyStateMachineToState: SecCKKSZoneKeyStateError withError: localerror];
327 // Everything is groovy. HOWEVER, we might still not have processed the keys. Ask for that!
328 [strongCKKS _onqueueKeyStateMachineRequestProcess];
329 [strongCKKS _onqueueAdvanceKeyStateMachineToState: SecCKKSZoneKeyStateReady withError: nil];
332 // ERROR. This isn't a total-failure error state, but one that should kick off a healing process.
333 ckkserror("ckksheal", strongCKKS, "couldn't save new key hierarchy to CloudKit: %@", error);
334 [strongCKKS _onqueueAdvanceKeyStateMachineToState: SecCKKSZoneKeyStateNewTLKsFailed withError: nil];
339 // Notify that we're done
340 [strongSelf.operationQueue addOperation: strongSelf.cloudkitModifyOperationFinished];
343 [ckks.database addOperation: modifyRecordsOp];
347 [keyset.tlk loadKeyMaterialFromKeychain:&error];
348 if(error && [ckks.lockStateTracker isLockedError:error]) {
349 ckksnotice("ckkskey", ckks, "Failed to load TLK from keychain, keybag is locked. Entering WaitForUnlock: %@", error);
350 [ckks _onqueueAdvanceKeyStateMachineToState:SecCKKSZoneKeyStateWaitForUnlock withError:nil];
353 ckkserror("ckksheal", ckks, "No TLK in keychain, triggering move to bad state: %@", error);
354 [ckks _onqueueAdvanceKeyStateMachineToState: SecCKKSZoneKeyStateWaitForTLK withError: nil];
358 if(![self ensureKeyPresent:keyset.classA]) {
362 if(![self ensureKeyPresent:keyset.classC]) {
366 // Seems good to us. Check if we're ready?
367 [ckks _onqueueAdvanceKeyStateMachineToState: SecCKKSZoneKeyStateReady withError: nil];
373 - (bool)ensureKeyPresent:(CKKSKey*)key {
374 NSError* error = nil;
375 CKKSKeychainView* ckks = self.ckks;
377 [key loadKeyMaterialFromKeychain:&error];
379 ckkserror("ckksheal", ckks, "Couldn't load classC key from keychain. Attempting recovery: %@", error);
381 [key unwrapViaKeyHierarchy: &error];
383 ckkserror("ckksheal", ckks, "Couldn't unwrap class C key using key hierarchy. Keys are broken, quitting: %@", error);
384 [ckks _onqueueAdvanceKeyStateMachineToState: SecCKKSZoneKeyStateError withError: error];
388 [key saveKeyMaterialToKeychain:&error];
390 ckkserror("ckksheal", ckks, "Couldn't save class C key to keychain: %@", error);
391 [ckks _onqueueAdvanceKeyStateMachineToState: SecCKKSZoneKeyStateError withError: error];
400 [self.cloudkitModifyOperationFinished cancel];