/ keychain / ckks / CKKSUpdateDeviceStateOperation.m
CKKSUpdateDeviceStateOperation.m
  1  /*
  2   * Copyright (c) 2017 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  #if OCTAGON
 25  
 26  #include <utilities/SecInternalReleasePriv.h>
 27  #import "keychain/ckks/CKKSKeychainView.h"
 28  #import "keychain/ckks/CKKSUpdateDeviceStateOperation.h"
 29  #import "keychain/ckks/CKKSCurrentKeyPointer.h"
 30  #import "keychain/ckks/CKKSKey.h"
 31  #import "keychain/ckks/CKKSLockStateTracker.h"
 32  #import "keychain/ckks/CKKSSQLDatabaseObject.h"
 33  #import "keychain/ot/ObjCImprovements.h"
 34  
 35  @interface CKKSUpdateDeviceStateOperation ()
 36  @property CKModifyRecordsOperation* modifyRecordsOperation;
 37  @property CKOperationGroup* group;
 38  @property bool rateLimit;
 39  @end
 40  
 41  @implementation CKKSUpdateDeviceStateOperation
 42  
 43  - (instancetype)initWithCKKSKeychainView:(CKKSKeychainView*)ckks rateLimit:(bool)rateLimit ckoperationGroup:(CKOperationGroup*)group {
 44      if((self = [super init])) {
 45          _ckks = ckks;
 46          _group = group;
 47          _rateLimit = rateLimit;
 48      }
 49      return self;
 50  }
 51  
 52  - (void)groupStart {
 53      CKKSKeychainView* ckks = self.ckks;
 54      if(!ckks) {
 55          ckkserror("ckksdevice", ckks, "no CKKS object");
 56          self.error = [NSError errorWithDomain:@"securityd" code:errSecInternalError userInfo:@{NSLocalizedDescriptionKey: @"no CKKS object"}];
 57          return;
 58      }
 59  
 60      CKKSAccountStateTracker* accountTracker = ckks.accountTracker;
 61      if(!accountTracker) {
 62          ckkserror("ckksdevice", ckks, "no AccountTracker object");
 63          self.error = [NSError errorWithDomain:@"securityd" code:errSecInternalError userInfo:@{NSLocalizedDescriptionKey: @"no AccountTracker object"}];
 64          return;
 65      }
 66  
 67      WEAKIFY(self);
 68  
 69      // We must have the ck device ID to run this operation.
 70      if([accountTracker.ckdeviceIDInitialized wait:200*NSEC_PER_SEC]) {
 71          ckkserror("ckksdevice", ckks, "CK device ID not initialized, quitting");
 72          self.error = [NSError errorWithDomain:@"securityd" code:errSecInternalError userInfo:@{NSLocalizedDescriptionKey: @"CK device ID not initialized"}];
 73          return;
 74      }
 75  
 76      NSString* ckdeviceID = accountTracker.ckdeviceID;
 77      if(!ckdeviceID) {
 78          ckkserror("ckksdevice", ckks, "CK device ID not initialized, quitting");
 79          self.error = [NSError errorWithDomain:@"securityd"
 80                                           code:errSecInternalError
 81                                       userInfo:@{NSLocalizedDescriptionKey: @"CK device ID null", NSUnderlyingErrorKey:CKKSNilToNSNull(accountTracker.ckdeviceIDError)}];
 82          return;
 83      }
 84  
 85      // We'd also really like to know the HSA2-ness of the world
 86      if([accountTracker.hsa2iCloudAccountInitialized wait:500*NSEC_PER_MSEC]) {
 87          ckkserror("ckksdevice", ckks, "Not quite sure if the account isa HSA2 or not. Probably will quit?");
 88      }
 89  
 90      [ckks dispatchSyncWithSQLTransaction:^CKKSDatabaseTransactionResult{
 91          NSError* error = nil;
 92  
 93          CKKSDeviceStateEntry* cdse = [ckks _onqueueCurrentDeviceStateEntry:&error];
 94          if(error || !cdse) {
 95              ckkserror("ckksdevice", ckks, "Error creating device state entry; quitting: %@", error);
 96              return CKKSDatabaseTransactionRollback;
 97          }
 98  
 99          if(self.rateLimit) {
100              NSDate* lastUpdate = cdse.storedCKRecord.modificationDate;
101  
102              // Only upload this every 3 days (1 day for internal installs)
103              NSDate* now = [NSDate date];
104              NSDateComponents* offset = [[NSDateComponents alloc] init];
105              if(SecIsInternalRelease()) {
106                  [offset setHour:-23];
107              } else {
108                  [offset setHour:-3*24];
109              }
110              NSDate* deadline = [[NSCalendar currentCalendar] dateByAddingComponents:offset toDate:now options:0];
111  
112              if(lastUpdate == nil || [lastUpdate compare: deadline] == NSOrderedAscending) {
113                  ckksnotice("ckksdevice", ckks, "Not rate-limiting: last updated %@ vs %@", lastUpdate, deadline);
114              } else {
115                  ckksnotice("ckksdevice", ckks, "Last update is within 3 days (%@); rate-limiting this operation", lastUpdate);
116                  self.error =  [NSError errorWithDomain:@"securityd"
117                                                    code:errSecInternalError
118                                                userInfo:@{NSLocalizedDescriptionKey: @"Rate-limited the CKKSUpdateDeviceStateOperation"}];
119                  return CKKSDatabaseTransactionRollback;
120              }
121          }
122  
123          ckksnotice("ckksdevice", ckks, "Saving new device state %@", cdse);
124  
125          NSArray* recordsToSave = @[[cdse CKRecordWithZoneID:ckks.zoneID]];
126  
127          // Start a CKModifyRecordsOperation to save this new/updated record.
128          NSBlockOperation* modifyComplete = [[NSBlockOperation alloc] init];
129          modifyComplete.name = @"updateDeviceState-modifyRecordsComplete";
130          [self dependOnBeforeGroupFinished: modifyComplete];
131  
132          self.modifyRecordsOperation = [[CKModifyRecordsOperation alloc] initWithRecordsToSave:recordsToSave recordIDsToDelete:nil];
133          self.modifyRecordsOperation.atomic = TRUE;
134          self.modifyRecordsOperation.qualityOfService = NSQualityOfServiceUtility;
135          self.modifyRecordsOperation.savePolicy = CKRecordSaveAllKeys; // Overwrite anything in CloudKit: this is our state now
136          self.modifyRecordsOperation.group = self.group;
137  
138          self.modifyRecordsOperation.perRecordCompletionBlock = ^(CKRecord *record, NSError * _Nullable error) {
139              STRONGIFY(self);
140              CKKSKeychainView* blockCKKS = self.ckks;
141  
142              if(!error) {
143                  ckksnotice("ckksdevice", blockCKKS, "Device state record upload successful for %@: %@", record.recordID.recordName, record);
144              } else {
145                  ckkserror("ckksdevice", blockCKKS, "error on row: %@ %@", error, record);
146              }
147          };
148  
149          self.modifyRecordsOperation.modifyRecordsCompletionBlock = ^(NSArray<CKRecord *> *savedRecords, NSArray<CKRecordID *> *deletedRecordIDs, NSError *ckerror) {
150              STRONGIFY(self);
151              CKKSKeychainView* strongCKKS = self.ckks;
152              if(!self || !strongCKKS) {
153                  ckkserror("ckksdevice", strongCKKS, "received callback for released object");
154                  self.error = [NSError errorWithDomain:@"securityd" code:errSecInternalError userInfo:@{NSLocalizedDescriptionKey: @"no CKKS object"}];
155                  [self runBeforeGroupFinished:modifyComplete];
156                  return;
157              }
158  
159              if(ckerror) {
160                  ckkserror("ckksdevice", strongCKKS, "CloudKit returned an error: %@", ckerror);
161                  self.error = ckerror;
162                  [self runBeforeGroupFinished:modifyComplete];
163                  return;
164              }
165  
166              __block NSError* error = nil;
167  
168              [strongCKKS dispatchSyncWithSQLTransaction:^CKKSDatabaseTransactionResult{
169                  for(CKRecord* record in savedRecords) {
170                      // Save the item records
171                      if([record.recordType isEqualToString: SecCKRecordDeviceStateType]) {
172                          CKKSDeviceStateEntry* newcdse = [[CKKSDeviceStateEntry alloc] initWithCKRecord:record];
173                          [newcdse saveToDatabase:&error];
174                          if(error) {
175                              ckkserror("ckksdevice", strongCKKS, "Couldn't save new device state(%@) to database: %@", newcdse, error);
176                          }
177                      }
178                  }
179                  return CKKSDatabaseTransactionCommit;
180              }];
181  
182              self.error = error;
183              [self runBeforeGroupFinished:modifyComplete];
184          };
185  
186          [self dependOnBeforeGroupFinished: self.modifyRecordsOperation];
187          [ckks.operationDependencies.ckdatabase addOperation:self.modifyRecordsOperation];
188  
189          return CKKSDatabaseTransactionCommit;
190      }];
191  }
192  
193  @end
194  
195  #endif // OCTAGON