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@
26 #import <CloudKit/CloudKit.h>
27 #import <CloudKit/CloudKit_Private.h>
29 #import "keychain/ckks/CKKSKeychainView.h"
30 #import "keychain/ckks/CKKSCurrentKeyPointer.h"
31 #import "keychain/ckks/CKKSKey.h"
32 #import "keychain/ckks/CKKSHealTLKSharesOperation.h"
33 #import "keychain/ckks/CKKSGroupOperation.h"
34 #import "keychain/ckks/CKKSTLKShareRecord.h"
35 #import "keychain/ot/ObjCImprovements.h"
37 #import "CKKSPowerCollection.h"
39 @interface CKKSHealTLKSharesOperation ()
40 @property NSBlockOperation* cloudkitModifyOperationFinished;
43 @implementation CKKSHealTLKSharesOperation
44 @synthesize intendedState = _intendedState;
45 @synthesize nextState = _nextState;
47 - (instancetype)init {
51 - (instancetype)initWithOperationDependencies:(CKKSOperationDependencies*)operationDependencies
52 ckks:(CKKSKeychainView*)ckks
54 if(self = [super init]) {
56 _deps = operationDependencies;
58 _nextState = SecCKKSZoneKeyStateHealTLKSharesFailed;
59 _intendedState = SecCKKSZoneKeyStateBecomeReady;
66 * We've been invoked because something is wonky with the tlk shares.
68 * Attempt to figure out what it is, and what we can do about it.
74 ckksnotice("ckksshare", self.deps.zoneID, "CKKSHealTLKSharesOperation cancelled, quitting");
78 NSArray<CKKSPeerProviderState*>* trustStates = [self.deps currentTrustStates];
81 __block CKKSCurrentKeySet* keyset = nil;
83 [self.deps.databaseProvider dispatchSyncWithReadOnlySQLTransaction:^{
84 keyset = [CKKSCurrentKeySet loadForZone:self.deps.zoneID];
88 self.nextState = SecCKKSZoneKeyStateUnhealthy;
89 self.error = keyset.error;
90 ckkserror("ckksshare", self.deps.zoneID, "couldn't load current keys: can't fix TLK shares");
93 ckksnotice("ckksshare", self.deps.zoneID, "Key set is %@", keyset);
96 [CKKSPowerCollection CKKSPowerEvent:kCKKSPowerEventTLKShareProcessing zone:self.deps.zoneID.zoneName];
98 // Okay! Perform the checks.
99 if(![keyset.tlk loadKeyMaterialFromKeychain:&error] || error) {
100 // Well, that's no good. We can't share a TLK we don't have.
101 if([self.deps.lockStateTracker isLockedError: error]) {
102 ckkserror("ckksshare", self.deps.zoneID, "Keychain is locked: can't fix shares yet: %@", error);
103 self.nextState = SecCKKSZoneKeyStateBecomeReady;
105 ckkserror("ckksshare", self.deps.zoneID, "couldn't load current tlk from keychain: %@", error);
106 self.nextState = SecCKKSZoneKeyStateUnhealthy;
111 NSSet<CKKSTLKShareRecord*>* newShares = [CKKSHealTLKSharesOperation createMissingKeyShares:keyset
112 trustStates:trustStates
115 ckkserror("ckksshare", self.deps.zoneID, "Unable to create shares: %@", error);
116 self.nextState = SecCKKSZoneKeyStateUnhealthy;
120 if(newShares.count == 0u) {
121 ckksnotice("ckksshare", self.deps.zoneID, "Don't believe we need to change any TLKShares, stopping");
122 self.nextState = self.intendedState;
126 keyset.pendingTLKShares = [newShares allObjects];
128 // Let's double-check: if we upload these TLKShares, will the world be right?
129 BOOL newSharesSufficient = [CKKSHealTLKSharesOperation areNewSharesSufficient:keyset
130 trustStates:trustStates
132 if(!newSharesSufficient || error) {
133 ckksnotice("ckksshare", self.deps.zoneID, "New shares won't resolve the share issue; erroring to avoid infinite loops");
134 self.nextState = SecCKKSZoneKeyStateError;
138 // Fire up our CloudKit operation!
140 NSMutableArray<CKRecord *>* recordsToSave = [[NSMutableArray alloc] init];
141 NSMutableArray<CKRecordID *>* recordIDsToDelete = [[NSMutableArray alloc] init];
142 NSMutableDictionary<CKRecordID*, CKRecord*>* attemptedRecords = [[NSMutableDictionary alloc] init];
144 ckksnotice("ckksshare", self.deps.zoneID, "Uploading %d new TLKShares", (unsigned int)newShares.count);
145 for(CKKSTLKShareRecord* share in newShares) {
146 ckksnotice("ckksshare", self.deps.zoneID, "Uploading TLKShare to %@ (as %@)", share.share.receiverPeerID, share.senderPeerID);
148 CKRecord* record = [share CKRecordWithZoneID:self.deps.zoneID];
149 [recordsToSave addObject: record];
150 attemptedRecords[record.recordID] = record;
153 // Use the spare operation trick to wait for the CKModifyRecordsOperation to complete
154 self.cloudkitModifyOperationFinished = [NSBlockOperation named:@"heal-tlkshares-modify-operation-finished" withBlock:^{}];
155 [self dependOnBeforeGroupFinished: self.cloudkitModifyOperationFinished];
157 // Get the CloudKit operation ready...
158 CKModifyRecordsOperation* modifyRecordsOp = [[CKModifyRecordsOperation alloc] initWithRecordsToSave:recordsToSave
159 recordIDsToDelete:recordIDsToDelete];
160 modifyRecordsOp.atomic = YES;
161 modifyRecordsOp.longLived = NO;
163 // very important: get the TLKShares off-device ASAP
164 modifyRecordsOp.configuration.automaticallyRetryNetworkFailures = NO;
165 modifyRecordsOp.configuration.discretionaryNetworkBehavior = CKOperationDiscretionaryNetworkBehaviorNonDiscretionary;
166 modifyRecordsOp.configuration.isCloudKitSupportOperation = YES;
168 modifyRecordsOp.group = self.deps.ckoperationGroup;
169 ckksnotice("ckksshare", self.deps.zoneID, "Operation group is %@", self.deps.ckoperationGroup);
171 modifyRecordsOp.perRecordCompletionBlock = ^(CKRecord *record, NSError * _Nullable error) {
174 // These should all fail or succeed as one. Do the hard work in the records completion block.
176 ckksnotice("ckksshare", self.deps.zoneID, "Successfully completed upload for record %@", record.recordID.recordName);
178 ckkserror("ckksshare", self.deps.zoneID, "error on row: %@ %@", record.recordID, error);
182 modifyRecordsOp.modifyRecordsCompletionBlock = ^(NSArray<CKRecord *> *savedRecords, NSArray<CKRecordID *> *deletedRecordIDs, NSError *error) {
185 CKKSKeychainView* strongCKKS = self.ckks;
187 [self.deps.databaseProvider dispatchSyncWithSQLTransaction:^CKKSDatabaseTransactionResult{
189 // Success. Persist the records to the CKKS database
190 ckksnotice("ckksshare", self.deps.zoneID, "Completed TLK Share heal operation with success");
191 NSError* localerror = nil;
193 // Save the new CKRecords to the database
194 for(CKRecord* record in savedRecords) {
195 CKKSTLKShareRecord* savedShare = [[CKKSTLKShareRecord alloc] initWithCKRecord:record];
196 bool saved = [savedShare saveToDatabase:&localerror];
198 if(!saved || localerror != nil) {
199 // erroring means we were unable to save the new TLKShare records to the database. This will cause us to try to reupload them. Fail.
200 // No recovery from this, really...
201 ckkserror("ckksshare", self.deps.zoneID, "Couldn't save new TLKShare record to database: %@", localerror);
202 self.error = localerror;
203 self.nextState = SecCKKSZoneKeyStateError;
204 return CKKSDatabaseTransactionCommit;
207 ckksnotice("ckksshare", self.deps.zoneID, "Successfully completed upload for %@", savedShare);
211 // Successfully sharing TLKs means we're now in ready!
212 self.nextState = SecCKKSZoneKeyStateBecomeReady;
214 ckkserror("ckksshare", self.deps.zoneID, "Completed TLK Share heal operation with error: %@", error);
215 [strongCKKS _onqueueCKWriteFailed:error attemptedRecordsChanged:attemptedRecords];
216 // Send the key state machine into tlksharesfailed
217 self.nextState = SecCKKSZoneKeyStateHealTLKSharesFailed;
219 return CKKSDatabaseTransactionCommit;
222 // Notify that we're done
223 [self.operationQueue addOperation: self.cloudkitModifyOperationFinished];
226 [self.ckks.database addOperation:modifyRecordsOp];
230 [self.cloudkitModifyOperationFinished cancel];
234 + (BOOL)areNewSharesSufficient:(CKKSCurrentKeySet*)keyset
235 trustStates:(NSArray<CKKSPeerProviderState*>*)trustStates
236 error:(NSError* __autoreleasing*)error
238 for(CKKSPeerProviderState* trustState in trustStates) {
239 NSError* localError = nil;
240 NSSet<id<CKKSPeer>>* peersMissingShares = [trustState findPeersMissingTLKSharesFor:keyset
242 if(peersMissingShares == nil || localError) {
243 if(trustState.essential) {
249 ckksnotice("ckksshare", keyset.tlk, "Failed to find peers for nonessential system: %@", trustState);
250 // Not a hard failure.
254 if(peersMissingShares.count > 0) {
255 ckksnotice("ckksshare", keyset.tlk, "New share set is missing shares for peers: %@", peersMissingShares);
264 + (NSSet<CKKSTLKShareRecord*>* _Nullable)createMissingKeyShares:(CKKSCurrentKeySet*)keyset
265 trustStates:(NSArray<CKKSPeerProviderState*>*)trustStates
266 error:(NSError* __autoreleasing*)error
268 NSError* localerror = nil;
269 NSSet<CKKSTLKShareRecord*>* newShares = nil;
271 // If any one of our trust states succeed, this function doesn't have an error
272 for(CKKSPeerProviderState* trustState in trustStates) {
273 NSError* stateError = nil;
275 NSSet<CKKSTLKShareRecord*>* newTrustShares = [self createMissingKeyShares:keyset
280 if(newTrustShares && !stateError) {
281 newShares = newShares ? [newShares setByAddingObjectsFromSet:newTrustShares] : newTrustShares;
283 ckksnotice("ckksshare", keyset.tlk, "Unable to create shares for trust set %@: %@", trustState, stateError);
284 if(localerror == nil) {
285 localerror = stateError;
290 // Only report an error if none of the trust states were able to succeed
294 if(error && localerror) {
301 + (NSSet<CKKSTLKShareRecord*>*)createMissingKeyShares:(CKKSCurrentKeySet*)keyset
302 peers:(CKKSPeerProviderState*)trustState
303 error:(NSError* __autoreleasing*)error
305 NSError* localerror = nil;
306 if(![keyset.tlk ensureKeyLoaded:&localerror]) {
307 ckkserror("ckksshare", keyset.tlk, "TLK not loaded; cannot make shares for peers: %@", localerror);
314 NSSet<id<CKKSPeer>>* remainingPeers = [trustState findPeersMissingTLKSharesFor:keyset
316 if(!remainingPeers) {
317 ckkserror("ckksshare", keyset.tlk, "Unable to find peers missing TLKShares: %@", localerror);
324 NSMutableSet<CKKSTLKShareRecord*>* newShares = [NSMutableSet set];
326 for(id<CKKSPeer> peer in remainingPeers) {
327 if(!peer.publicEncryptionKey) {
328 ckksnotice("ckksshare", keyset.tlk, "No need to make TLK for %@; they don't have any encryption keys", peer);
332 // Create a share for this peer.
333 ckksnotice("ckksshare", keyset.tlk, "Creating share of %@ as %@ for %@", keyset.tlk, trustState.currentSelfPeers.currentSelf, peer);
334 CKKSTLKShareRecord* newShare = [CKKSTLKShareRecord share:keyset.tlk
335 as:trustState.currentSelfPeers.currentSelf
342 ckkserror("ckksshare", keyset.tlk, "Couldn't create new share for %@: %@", peer, localerror);
349 [newShares addObject: newShare];