]> git.saurik.com Git - apple/security.git/blob - keychain/ckks/CKKSNewTLKOperation.m
Security-58286.1.32.tar.gz
[apple/security.git] / keychain / ckks / CKKSNewTLKOperation.m
1 /*
2 * Copyright (c) 2016 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 "CKKSNewTLKOperation.h"
28 #import "CKKSGroupOperation.h"
29 #import "CKKSNearFutureScheduler.h"
30 #import "keychain/ckks/CloudKitCategories.h"
31
32 #if OCTAGON
33
34 @interface CKKSNewTLKOperation ()
35 @property NSBlockOperation* cloudkitModifyOperationFinished;
36 @property CKOperationGroup* ckoperationGroup;
37 @end
38
39 @implementation CKKSNewTLKOperation
40
41 - (instancetype)init {
42 return nil;
43 }
44 - (instancetype)initWithCKKSKeychainView:(CKKSKeychainView*)ckks ckoperationGroup:(CKOperationGroup*)ckoperationGroup {
45 if(self = [super init]) {
46 _ckks = ckks;
47 _ckoperationGroup = ckoperationGroup;
48 }
49 return self;
50 }
51
52 - (void)groupStart {
53 /*
54 * Rolling keys is an essential operation, and must be transactional: either completing successfully or
55 * failing entirely. Also, in the case of failure, some other peer has beaten us to CloudKit and changed
56 * the keys stored there (which we must now fetch and handle): the keys we attempted to upload are useless.
57
58 * Therefore, we'll skip the normal OutgoingQueue behavior, and persist keys in-memory until such time as
59 * CloudKit tells us the operation succeeds or fails, at which point we'll commit them or throw them away.
60 *
61 * Note that this means edge cases in the case of secd dying in the middle of this operation; our normal
62 * retry mechanisms won't work. We'll have to make the policy decision to re-roll the keys if needed upon
63 * the next launch of secd (or, the write will succeed after we die, and we'll handle receiving the CK
64 * items as if a different peer uploaded them).
65 */
66
67 __weak __typeof(self) weakSelf = self;
68
69 CKKSKeychainView* ckks = self.ckks;
70
71 if(self.cancelled) {
72 ckksnotice("ckkstlk", ckks, "CKKSNewTLKOperation cancelled, quitting");
73 return;
74 }
75
76 if(!ckks) {
77 ckkserror("ckkstlk", ckks, "no CKKS object");
78 return;
79 }
80
81 // Synchronous, on some thread. Get back on the CKKS queue for SQL thread-safety.
82 [ckks dispatchSync: ^bool{
83 if(self.cancelled) {
84 ckksnotice("ckkstlk", ckks, "CKKSNewTLKOperation cancelled, quitting");
85 return false;
86 }
87
88 ckks.lastNewTLKOperation = self;
89
90 NSError* error = nil;
91
92 ckksinfo("ckkstlk", ckks, "Generating new TLK");
93
94 // Promote to strong reference
95 CKKSKeychainView* ckks = self.ckks;
96
97 CKKSKey* newTLK = nil;
98 CKKSKey* newClassAKey = nil;
99 CKKSKey* newClassCKey = nil;
100 CKKSKey* wrappedOldTLK = nil;
101
102 NSMutableArray<CKRecord *>* recordsToSave = [[NSMutableArray alloc] init];
103 NSMutableArray<CKRecordID *>* recordIDsToDelete = [[NSMutableArray alloc] init];
104
105 // Now, prepare data for the operation:
106
107 // We must find the current TLK (to wrap it to the new TLK).
108 NSError* localerror = nil;
109 CKKSKey* oldTLK = [CKKSKey currentKeyForClass: SecCKKSKeyClassTLK zoneID:ckks.zoneID error: &localerror];
110 if(localerror) {
111 ckkserror("ckkstlk", ckks, "couldn't load the current TLK: %@", localerror);
112 // TODO: not loading the old TLK is fine, but only if there aren't any TLKs
113 }
114
115 [oldTLK ensureKeyLoaded: &error];
116
117 ckksnotice("ckkstlk", ckks, "Old TLK is: %@ %@", oldTLK, error);
118 if(error != nil) {
119 ckkserror("ckkstlk", ckks, "Couldn't fetch and unwrap old TLK: %@", error);
120 [ckks _onqueueAdvanceKeyStateMachineToState: SecCKKSZoneKeyStateError withError: error];
121 return false;
122 }
123
124 // Generate new hierarchy:
125 // newTLK
126 // / | \
127 // / | \
128 // / | \
129 // oldTLK classA classC
130
131 newTLK = [[CKKSKey alloc] initSelfWrappedWithAESKey:[CKKSAESSIVKey randomKey]
132 uuid:[[NSUUID UUID] UUIDString]
133 keyclass:SecCKKSKeyClassTLK
134 state:SecCKKSProcessedStateLocal
135 zoneID:ckks.zoneID
136 encodedCKRecord:nil
137 currentkey:true];
138
139 newClassAKey = [CKKSKey randomKeyWrappedByParent: newTLK keyclass: SecCKKSKeyClassA error: &error];
140 newClassCKey = [CKKSKey randomKeyWrappedByParent: newTLK keyclass: SecCKKSKeyClassC error: &error];
141
142 if(error != nil) {
143 ckkserror("ckkstlk", ckks, "couldn't make new key hierarchy: %@", error);
144 // TODO: this really isn't the error state, but a 'retry'.
145 [ckks _onqueueAdvanceKeyStateMachineToState: SecCKKSZoneKeyStateError withError: error];
146 return false;
147 }
148
149 CKKSCurrentKeyPointer* currentTLKPointer = [CKKSCurrentKeyPointer forKeyClass: SecCKKSKeyClassTLK withKeyUUID:newTLK.uuid zoneID:ckks.zoneID error: &error];
150 CKKSCurrentKeyPointer* currentClassAPointer = [CKKSCurrentKeyPointer forKeyClass: SecCKKSKeyClassA withKeyUUID:newClassAKey.uuid zoneID:ckks.zoneID error: &error];
151 CKKSCurrentKeyPointer* currentClassCPointer = [CKKSCurrentKeyPointer forKeyClass: SecCKKSKeyClassC withKeyUUID:newClassCKey.uuid zoneID:ckks.zoneID error: &error];
152
153 if(error != nil) {
154 ckkserror("ckkstlk", ckks, "couldn't make current key records: %@", error);
155 // TODO: this really isn't the error state, but a 'retry'.
156 [ckks _onqueueAdvanceKeyStateMachineToState: SecCKKSZoneKeyStateError withError: error];
157 return false;
158 }
159
160 // Wrap old TLK under the new TLK
161 wrappedOldTLK = [oldTLK copy];
162 if(wrappedOldTLK) {
163 [wrappedOldTLK ensureKeyLoaded: &error];
164 if(error != nil) {
165 ckkserror("ckkstlk", ckks, "couldn't unwrap TLK, aborting new TLK operation: %@", error);
166 [ckks _onqueueAdvanceKeyStateMachineToState: SecCKKSZoneKeyStateError withError: error];
167 return false;
168 }
169
170 [wrappedOldTLK wrapUnder: newTLK error:&error];
171 // TODO: should we continue in this error state? Might be required to fix broken TLKs/argue over which TLK should be used
172 if(error != nil) {
173 ckkserror("ckkstlk", ckks, "couldn't wrap oldTLK, aborting new TLK operation: %@", error);
174 [ckks _onqueueAdvanceKeyStateMachineToState: SecCKKSZoneKeyStateError withError: error];
175 return false;
176 }
177
178 wrappedOldTLK.currentkey = false;
179 }
180
181 [recordsToSave addObject: [newTLK CKRecordWithZoneID: ckks.zoneID]];
182 [recordsToSave addObject: [newClassAKey CKRecordWithZoneID: ckks.zoneID]];
183 [recordsToSave addObject: [newClassCKey CKRecordWithZoneID: ckks.zoneID]];
184
185 [recordsToSave addObject: [currentTLKPointer CKRecordWithZoneID: ckks.zoneID]];
186 [recordsToSave addObject: [currentClassAPointer CKRecordWithZoneID: ckks.zoneID]];
187 [recordsToSave addObject: [currentClassCPointer CKRecordWithZoneID: ckks.zoneID]];
188
189 if(wrappedOldTLK) {
190 [recordsToSave addObject: [wrappedOldTLK CKRecordWithZoneID: ckks.zoneID]];
191 }
192
193 // Save the proposed keys to the keychain. Note that we might reject this TLK later, but in that case, this TLK is just orphaned. No worries!
194 ckksinfo("ckkstlk", ckks, "Saving new keys %@ to database %@", recordsToSave, ckks.database);
195
196 [newTLK saveKeyMaterialToKeychain: &error];
197 [newClassAKey saveKeyMaterialToKeychain: &error];
198 [newClassCKey saveKeyMaterialToKeychain: &error];
199 if(error) {
200 self.error = error;
201 ckkserror("ckkstlk", ckks, "couldn't save new key material to keychain, aborting new TLK operation: %@", error);
202 [ckks _onqueueAdvanceKeyStateMachineToState: SecCKKSZoneKeyStateError withError: error];
203 return false;
204 }
205
206 // Use the spare operation trick to wait for the CKModifyRecordsOperation to complete
207 self.cloudkitModifyOperationFinished = [NSBlockOperation named:@"newtlk-cloudkit-modify-operation-finished" withBlock:^{}];
208 [self dependOnBeforeGroupFinished: self.cloudkitModifyOperationFinished];
209
210 CKModifyRecordsOperation* modifyRecordsOp = nil;
211
212 // Get the CloudKit operation ready...
213 modifyRecordsOp = [[CKModifyRecordsOperation alloc] initWithRecordsToSave:recordsToSave recordIDsToDelete:recordIDsToDelete];
214 modifyRecordsOp.atomic = YES;
215 modifyRecordsOp.longLived = NO; // The keys are only in memory; mark this explicitly not long-lived
216 modifyRecordsOp.timeoutIntervalForRequest = 2;
217 modifyRecordsOp.qualityOfService = NSQualityOfServiceUtility; // relatively important. Use Utility.
218 modifyRecordsOp.group = self.ckoperationGroup;
219 ckksnotice("ckkstlk", ckks, "Operation group is %@", self.ckoperationGroup);
220
221 NSMutableDictionary<CKRecordID*, CKRecord*>* attemptedRecords = [[NSMutableDictionary alloc] init];
222 for(CKRecord* record in recordsToSave) {
223 attemptedRecords[record] = record;
224 }
225
226 modifyRecordsOp.perRecordCompletionBlock = ^(CKRecord *record, NSError * _Nullable error) {
227 __strong __typeof(weakSelf) strongSelf = weakSelf;
228 __strong __typeof(strongSelf.ckks) blockCKKS = strongSelf.ckks;
229
230 // These should all fail or succeed as one. Do the hard work in the records completion block.
231 if(!error) {
232 ckksnotice("ckkstlk", blockCKKS, "Successfully completed upload for %@", record.recordID.recordName);
233 } else {
234 ckkserror("ckkstlk", blockCKKS, "error on row: %@ %@", error, record);
235 }
236 };
237
238
239 modifyRecordsOp.modifyRecordsCompletionBlock = ^(NSArray<CKRecord *> *savedRecords, NSArray<CKRecordID *> *deletedRecordIDs, NSError *ckerror) {
240 __strong __typeof(weakSelf) strongSelf = weakSelf;
241 __strong __typeof(strongSelf.ckks) strongCKKS = strongSelf.ckks;
242 if(!strongSelf || !strongCKKS) {
243 ckkserror("ckkstlk", strongCKKS, "received callback for released object");
244 return;
245 }
246
247 [strongCKKS dispatchSync: ^bool{
248 if(ckerror == nil) {
249 ckksnotice("ckkstlk", strongCKKS, "Completed TLK CloudKit operation");
250
251 // Success. Persist the keys to the CKKS database.
252 NSError* localerror = nil;
253
254 // Save the new CKRecords to the before persisting to database
255 for(CKRecord* record in savedRecords) {
256 if([newTLK matchesCKRecord: record]) {
257 newTLK.storedCKRecord = record;
258 } else if([newClassAKey matchesCKRecord: record]) {
259 newClassAKey.storedCKRecord = record;
260 } else if([newClassCKey matchesCKRecord: record]) {
261 newClassCKey.storedCKRecord = record;
262 } else if([wrappedOldTLK matchesCKRecord: record]) {
263 wrappedOldTLK.storedCKRecord = record;
264
265 } else if([currentTLKPointer matchesCKRecord: record]) {
266 currentTLKPointer.storedCKRecord = record;
267 } else if([currentClassAPointer matchesCKRecord: record]) {
268 currentClassAPointer.storedCKRecord = record;
269 } else if([currentClassCPointer matchesCKRecord: record]) {
270 currentClassCPointer.storedCKRecord = record;
271 }
272 }
273
274 [newTLK saveToDatabaseAsOnlyCurrentKeyForClassAndState: &localerror];
275 [newClassAKey saveToDatabaseAsOnlyCurrentKeyForClassAndState: &localerror];
276 [newClassCKey saveToDatabaseAsOnlyCurrentKeyForClassAndState: &localerror];
277
278 [currentTLKPointer saveToDatabase: &localerror];
279 [currentClassAPointer saveToDatabase: &localerror];
280 [currentClassCPointer saveToDatabase: &localerror];
281
282 [wrappedOldTLK saveToDatabase: &localerror];
283
284 // TLKs are already saved in the local keychain; fire off a backup
285 CKKSNearFutureScheduler* tlkNotifier = strongCKKS.savedTLKNotifier;
286 ckksnotice("ckkstlk", strongCKKS, "triggering new TLK notification: %@", tlkNotifier);
287 [tlkNotifier trigger];
288
289 if(localerror != nil) {
290 ckkserror("ckkstlk", strongCKKS, "couldn't save new key hierarchy to database; this is very bad: %@", localerror);
291 [strongCKKS _onqueueAdvanceKeyStateMachineToState: SecCKKSZoneKeyStateError withError: localerror];
292 return false;
293 } else {
294 // Everything is groovy.
295 [strongCKKS _onqueueAdvanceKeyStateMachineToState: SecCKKSZoneKeyStateReady withError: nil];
296 }
297 } else {
298 ckkserror("ckkstlk", strongCKKS, "couldn't save new key hierarchy to CloudKit: %@", ckerror);
299 [strongCKKS _onqueueAdvanceKeyStateMachineToState: SecCKKSZoneKeyStateNewTLKsFailed withError: nil];
300
301 [strongCKKS _onqueueCKWriteFailed:ckerror attemptedRecordsChanged:attemptedRecords];
302
303 // Delete these keys from the keychain only if CloudKit positively told us the write failed.
304 // Other failures _might_ leave the writes written to cloudkit; who can say?
305 if([ckerror ckksIsCKErrorRecordChangedError]) {
306 NSError* localerror = nil;
307 [newTLK deleteKeyMaterialFromKeychain: &localerror];
308 [newClassAKey deleteKeyMaterialFromKeychain: &localerror];
309 [newClassCKey deleteKeyMaterialFromKeychain: &localerror];
310 if(localerror) {
311 ckkserror("ckkstlk", strongCKKS, "couldn't delete now-useless key material from keychain: %@", localerror);
312 }
313 } else {
314 ckksnotice("ckkstlk", strongCKKS, "Error is too scary; not deleting likely-useless key material from keychain");
315 }
316 }
317 return true;
318 }];
319
320 // Notify that we're done
321 [strongSelf.operationQueue addOperation: strongSelf.cloudkitModifyOperationFinished];
322 };
323
324 [ckks.database addOperation: modifyRecordsOp];
325 return true;
326 }];
327 }
328
329 - (void)cancel {
330 [self.cloudkitModifyOperationFinished cancel];
331 [super cancel];
332 }
333
334 @end;
335
336 #endif