/ keychain / escrowrequest / EscrowRequestController.m
EscrowRequestController.m
  1  
  2  #import "utilities/debugging.h"
  3  
  4  #import "keychain/ot/OctagonStateMachine.h"
  5  #import "keychain/ot/OTStates.h"
  6  #import "keychain/escrowrequest/EscrowRequestController.h"
  7  #import "keychain/escrowrequest/EscrowRequestServer.h"
  8  
  9  #import "keychain/ckks/CKKSLockStateTracker.h"
 10  
 11  #import "keychain/ot/ObjCImprovements.h"
 12  
 13  #import "keychain/escrowrequest/operations/EscrowRequestInformCloudServicesOperation.h"
 14  #import "keychain/escrowrequest/operations/EscrowRequestPerformEscrowEnrollOperation.h"
 15  
 16  #import "keychain/escrowrequest/generated_source/SecEscrowPendingRecord.h"
 17  #import "keychain/escrowrequest/SecEscrowPendingRecord+KeychainSupport.h"
 18  
 19  OctagonState* const EscrowRequestStateNothingToDo = (OctagonState*)@"nothing_to_do";
 20  OctagonState* const EscrowRequestStateTriggerCloudServices = (OctagonState*)@"trigger_cloudservices";
 21  
 22  OctagonState* const EscrowRequestStateAttemptEscrowUpload = (OctagonState*)@"trigger_escrow_upload";
 23  OctagonState* const EscrowRequestStateWaitForUnlock = (OctagonState*)@"wait_for_unlock";
 24  
 25  @interface EscrowRequestController ()
 26  @property dispatch_queue_t queue;
 27  @property CKKSLockStateTracker* lockStateTracker;
 28  @property bool haveRecordedDate;
 29  @end
 30  
 31  @implementation EscrowRequestController
 32  
 33  - (instancetype)initWithLockStateTracker:(CKKSLockStateTracker*)lockStateTracker
 34  {
 35      if((self = [super init])) {
 36          _queue = dispatch_queue_create("EscrowRequestControllerQueue", DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL);
 37          _lockStateTracker = lockStateTracker;
 38  
 39          _stateMachine = [[OctagonStateMachine alloc] initWithName:@"escrowrequest"
 40                                                             states:[NSSet setWithArray:@[EscrowRequestStateNothingToDo,
 41                                                                                          EscrowRequestStateTriggerCloudServices,
 42                                                                                          EscrowRequestStateAttemptEscrowUpload,
 43                                                                                          EscrowRequestStateWaitForUnlock]]
 44                                                              flags: [NSSet setWithArray:@[OctagonFlagEscrowRequestInformCloudServicesOperation]]
 45                                                       initialState:EscrowRequestStateNothingToDo
 46                                                              queue:_queue
 47                                                        stateEngine:self
 48                                                   lockStateTracker:lockStateTracker
 49                                                reachabilityTracker:nil];
 50  
 51          _forceIgnoreCloudServicesRateLimiting = false;
 52      }
 53  
 54      return self;
 55  }
 56  
 57  - (CKKSResultOperation<OctagonStateTransitionOperationProtocol> * _Nullable)_onqueueNextStateMachineTransition:(nonnull OctagonState *)currentState
 58                                                                                                           flags:(nonnull OctagonFlags *)flags
 59                                                                                                    pendingFlags:(nonnull id<OctagonStateOnqueuePendingFlagHandler>)pendingFlagHandler
 60  {
 61      dispatch_assert_queue(self.queue);
 62      if([flags _onqueueContains:OctagonFlagEscrowRequestInformCloudServicesOperation]) {
 63          [flags _onqueueRemoveFlag:OctagonFlagEscrowRequestInformCloudServicesOperation];
 64          return [[EscrowRequestInformCloudServicesOperation alloc] initWithIntendedState:EscrowRequestStateNothingToDo
 65                                                                               errorState:EscrowRequestStateNothingToDo
 66                                                                         lockStateTracker:self.lockStateTracker];
 67      }
 68  
 69      if([currentState isEqualToString:EscrowRequestStateTriggerCloudServices]) {
 70          return [[EscrowRequestInformCloudServicesOperation alloc] initWithIntendedState:EscrowRequestStateNothingToDo
 71                                                                               errorState:EscrowRequestStateNothingToDo
 72                                                                         lockStateTracker:self.lockStateTracker];
 73      }
 74  
 75      if([currentState isEqualToString:EscrowRequestStateAttemptEscrowUpload]) {
 76          return [[EscrowRequestPerformEscrowEnrollOperation alloc] initWithIntendedState:EscrowRequestStateNothingToDo
 77                                                                               errorState:EscrowRequestStateNothingToDo
 78                                                                      enforceRateLimiting:true
 79                                                                         lockStateTracker:self.lockStateTracker];
 80      }
 81  
 82      if([currentState isEqualToString:EscrowRequestStateWaitForUnlock]) {
 83          secnotice("escrowrequest", "waiting for unlock before continuing with state machine");
 84          OctagonStateTransitionOperation* op = [OctagonStateTransitionOperation named:@"wait-for-unlock"
 85                                                                              entering:EscrowRequestStateNothingToDo];
 86          [op addNullableDependency:self.lockStateTracker.unlockDependency];
 87          return op;
 88      }
 89  
 90      NSError* error = nil;
 91      NSArray<SecEscrowPendingRecord*>* records = [SecEscrowPendingRecord loadAllFromKeychain:&error];
 92      if(error) {
 93          if([self.lockStateTracker isLockedError:error]) {
 94              return [OctagonStateTransitionOperation named:@"wait-for-unlock"
 95                                                   entering:EscrowRequestStateWaitForUnlock];
 96          }
 97          secnotice("escrowrequest", "failed to fetch records from keychain, nothing to do: %@", error);
 98          return nil;
 99      }
100  
101      // First, do we need to poke CloudServices?
102      for(SecEscrowPendingRecord* record in records) {
103          // Completed records don't need anything.
104          if(record.hasUploadCompleted && record.uploadCompleted) {
105              continue;
106          }
107  
108          if (!self.haveRecordedDate) {
109              NSDate *date = [[CKKSAnalytics logger] datePropertyForKey:ESRPendingSince];
110              if (date == NULL) {
111                  [[CKKSAnalytics logger] setDateProperty:[NSDate date] forKey:ESRPendingSince];
112              }
113              self.haveRecordedDate = true;
114          }
115  
116          uint64_t fiveMinutesAgo = ((uint64_t)[[NSDate date] timeIntervalSince1970] * 1000) - (1000*60*5);
117  
118          if(!record.certCached) {
119              if(!self.forceIgnoreCloudServicesRateLimiting && (record.hasLastCloudServicesTriggerTime && record.lastCloudServicesTriggerTime >= fiveMinutesAgo)) {
120                  secnotice("escrowrequest", "Request %@ needs to cache a certificate, but that has been attempted recently. Holding off...", record.uuid);
121                  continue;
122              }
123  
124              secnotice("escrowrequest", "Request %@ needs a cached certififcate", record.uuid);
125  
126              return [OctagonStateTransitionOperation named:@"escrow-request-cache-cert"
127                                                   entering:EscrowRequestStateTriggerCloudServices];
128          }
129  
130          if(record.hasSerializedPrerecord) {
131              if([record escrowAttemptedWithinLastSeconds:5*60]) {
132                  secnotice("escrowrequest", "Request %@ needs to be stored, but has been attempted recently. Holding off...", record.uuid);
133                  continue;
134              }
135  
136              secnotice("escrowrequest", "Request %@ needs to be stored!", record.uuid);
137  
138              return [OctagonStateTransitionOperation named:@"escrow-request-attempt-escrow-upload"
139                                                   entering:EscrowRequestStateAttemptEscrowUpload];
140          }
141      }
142  
143  
144      return nil;
145  }
146  
147  - (void)triggerEscrowUpdateRPC:(nonnull NSString *)reason
148                           reply:(nonnull void (^)(NSError * _Nullable))reply
149  {
150      [self.stateMachine startOperation];
151  
152      NSError* error = nil;
153      NSArray<SecEscrowPendingRecord*>* records = [SecEscrowPendingRecord loadAllFromKeychain:&error];
154      if(error && !([error.domain isEqualToString:NSOSStatusErrorDomain] && error.code == errSecItemNotFound)) {
155          secnotice("escrowrequest", "failed to fetch records from keychain: %@", error);
156          reply(error);
157          return;
158      }
159      error = nil;
160  
161      secnotice("escrowrequest", "Investigating a new escrow request");
162  
163      BOOL escrowRequestExists = NO;
164      for(SecEscrowPendingRecord* existingRecord in records) {
165          if(existingRecord.uploadCompleted) {
166              continue;
167          }
168  
169          if (existingRecord.hasAltDSID) {
170              continue;
171          }
172  
173          secnotice("escrowrequest", "Retriggering an existing escrow request: %@", existingRecord);
174          existingRecord.hasCertCached = false;
175          existingRecord.serializedPrerecord = nil;
176  
177          [existingRecord saveToKeychain:&error];
178          if(error) {
179              secerror("escrowrequest: Unable to save modified request to keychain: %@", error);
180              reply(error);
181              return;
182          }
183  
184          secnotice("escrowrequest", "Retriggering an existing escrow request complete");
185          escrowRequestExists = YES;
186      }
187  
188      if(escrowRequestExists == NO){
189          secnotice("escrowrequest", "Creating a new escrow request");
190  
191          SecEscrowPendingRecord* record = [[SecEscrowPendingRecord alloc] init];
192          record.uuid = [[NSUUID UUID] UUIDString];
193          record.altDSID = nil;
194          record.triggerRequestTime = ((uint64_t)[[NSDate date] timeIntervalSince1970] * 1000);
195          secnotice("escrowrequest", "beginning a new escrow request (%@)", record.uuid);
196  
197          [record saveToKeychain:&error];
198  
199          if(error) {
200              secerror("escrowrequest: unable to save escrow update request: %@", error);
201              reply(error);
202              return;
203          }
204      }
205  
206      [[CKKSAnalytics logger] setDateProperty:[NSDate date] forKey:ESRPendingSince];
207      self.haveRecordedDate = true;
208  
209      [self.stateMachine handleFlag:OctagonFlagEscrowRequestInformCloudServicesOperation];
210  
211      reply(nil);
212  }
213  
214  - (void)storePrerecordsInEscrowRPC:(void (^)(uint64_t count, NSError* _Nullable error))reply
215  {
216      EscrowRequestPerformEscrowEnrollOperation* op = [[EscrowRequestPerformEscrowEnrollOperation alloc] initWithIntendedState:EscrowRequestStateNothingToDo
217                                                                                                                    errorState:EscrowRequestStateNothingToDo
218                                                                                                           enforceRateLimiting:false
219                                                                                                              lockStateTracker:self.lockStateTracker];
220      [self.stateMachine startOperation];
221      [self.stateMachine doSimpleStateMachineRPC:@"trigger-escrow-store"
222                                              op:op
223                                    sourceStates:[NSSet setWithObject:EscrowRequestStateNothingToDo]
224                                           reply:^(NSError * _Nullable error) {
225                                               secnotice("escrowrequest", "Uploaded %d records with error %@", (int)op.numberOfRecordsUploaded, error);
226                                               reply(op.numberOfRecordsUploaded, error);
227                                           }];
228  }
229  
230  @end