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