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