CKKSCurrentKeyPointer.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 #import "CKKSCurrentKeyPointer.h" 25 26 #if OCTAGON 27 28 #import "keychain/ckks/CKKSStates.h" 29 #import "keychain/categories/NSError+UsefulConstructors.h" 30 31 @implementation CKKSCurrentKeyPointer 32 33 - (instancetype)initForClass:(CKKSKeyClass*)keyclass 34 currentKeyUUID:(NSString*)currentKeyUUID 35 zoneID:(CKRecordZoneID*)zoneID 36 encodedCKRecord: (NSData*) encodedrecord 37 { 38 if(self = [super initWithCKRecordType: SecCKRecordCurrentKeyType encodedCKRecord:encodedrecord zoneID:zoneID]) { 39 _keyclass = keyclass; 40 _currentKeyUUID = currentKeyUUID; 41 42 if(self.currentKeyUUID == nil) { 43 ckkserror_global("currentkey", "created a CKKSCurrentKey with a nil currentKeyUUID. Why?"); 44 } 45 } 46 return self; 47 } 48 49 - (NSString*)description { 50 return [NSString stringWithFormat:@"<CKKSCurrentKeyPointer(%@) %@: %@>", self.zoneID.zoneName, self.keyclass, self.currentKeyUUID]; 51 } 52 53 - (instancetype)copyWithZone:(NSZone*)zone { 54 CKKSCurrentKeyPointer* copy = [super copyWithZone:zone]; 55 copy.keyclass = [self.keyclass copyWithZone:zone]; 56 copy.currentKeyUUID = [self.currentKeyUUID copyWithZone:zone]; 57 return copy; 58 } 59 - (BOOL)isEqual: (id) object { 60 if(![object isKindOfClass:[CKKSCurrentKeyPointer class]]) { 61 return NO; 62 } 63 64 CKKSCurrentKeyPointer* obj = (CKKSCurrentKeyPointer*) object; 65 66 return ([self.zoneID isEqual: obj.zoneID] && 67 ((self.currentKeyUUID == nil && obj.currentKeyUUID == nil) || [self.currentKeyUUID isEqual: obj.currentKeyUUID]) && 68 ((self.keyclass == nil && obj.keyclass == nil) || [self.keyclass isEqual:obj.keyclass]) && 69 YES) ? YES : NO; 70 } 71 72 #pragma mark - CKKSCKRecordHolder methods 73 74 - (NSString*) CKRecordName { 75 return self.keyclass; 76 } 77 78 - (CKRecord*) updateCKRecord: (CKRecord*) record zoneID: (CKRecordZoneID*) zoneID { 79 if(![record.recordType isEqualToString: SecCKRecordCurrentKeyType]) { 80 @throw [NSException 81 exceptionWithName:@"WrongCKRecordTypeException" 82 reason:[NSString stringWithFormat: @"CKRecordType (%@) was not %@", record.recordType, SecCKRecordCurrentKeyType] 83 userInfo:nil]; 84 } 85 86 // The record name should already match keyclass... 87 if(![record.recordID.recordName isEqualToString: self.keyclass]) { 88 @throw [NSException 89 exceptionWithName:@"WrongCKRecordNameException" 90 reason:[NSString stringWithFormat: @"CKRecord name (%@) was not %@", record.recordID.recordName, self.keyclass] 91 userInfo:nil]; 92 } 93 94 // Set the parent reference 95 record[SecCKRecordParentKeyRefKey] = [[CKReference alloc] initWithRecordID: [[CKRecordID alloc] initWithRecordName: self.currentKeyUUID zoneID: zoneID] action: CKReferenceActionNone]; 96 return record; 97 } 98 99 - (bool) matchesCKRecord: (CKRecord*) record { 100 if(![record.recordType isEqualToString: SecCKRecordCurrentKeyType]) { 101 return false; 102 } 103 104 if(![record.recordID.recordName isEqualToString: self.keyclass]) { 105 return false; 106 } 107 108 if(![[record[SecCKRecordParentKeyRefKey] recordID].recordName isEqualToString: self.currentKeyUUID]) { 109 return false; 110 } 111 112 return true; 113 } 114 115 - (void) setFromCKRecord: (CKRecord*) record { 116 if(![record.recordType isEqualToString: SecCKRecordCurrentKeyType]) { 117 @throw [NSException 118 exceptionWithName:@"WrongCKRecordTypeException" 119 reason:[NSString stringWithFormat: @"CKRecordType (%@) was not %@", record.recordType, SecCKRecordCurrentKeyType] 120 userInfo:nil]; 121 } 122 123 [self setStoredCKRecord:record]; 124 125 // TODO: verify this is a real keyclass 126 self.keyclass = (CKKSKeyClass*) record.recordID.recordName; 127 self.currentKeyUUID = [record[SecCKRecordParentKeyRefKey] recordID].recordName; 128 129 if(self.currentKeyUUID == nil) { 130 ckkserror_global("currentkey", "No current key UUID in record! How/why? %@", record); 131 } 132 } 133 134 #pragma mark - Load from database 135 136 + (instancetype _Nullable)fromDatabase:(CKKSKeyClass*)keyclass zoneID:(CKRecordZoneID*)zoneID error:(NSError * __autoreleasing *)error 137 { 138 return [self fromDatabaseWhere: @{@"keyclass": keyclass, @"ckzone":zoneID.zoneName} error: error]; 139 } 140 141 + (instancetype _Nullable)tryFromDatabase:(CKKSKeyClass*)keyclass zoneID:(CKRecordZoneID*)zoneID error:(NSError * __autoreleasing *)error 142 { 143 return [self tryFromDatabaseWhere: @{@"keyclass": keyclass, @"ckzone":zoneID.zoneName} error: error]; 144 } 145 146 + (instancetype _Nullable)forKeyClass:(CKKSKeyClass*)keyclass withKeyUUID:(NSString*)keyUUID zoneID:(CKRecordZoneID*)zoneID error:(NSError * __autoreleasing *)error 147 { 148 NSError* localerror = nil; 149 CKKSCurrentKeyPointer* current = [self tryFromDatabase: keyclass zoneID:zoneID error: &localerror]; 150 if(localerror) { 151 if(error) { 152 *error = localerror; 153 } 154 return nil; 155 } 156 157 if(current) { 158 current.currentKeyUUID = keyUUID; 159 return current; 160 } 161 162 return [[CKKSCurrentKeyPointer alloc] initForClass: keyclass currentKeyUUID: keyUUID zoneID:zoneID encodedCKRecord:nil]; 163 } 164 165 + (NSArray<CKKSCurrentKeyPointer*>*)all:(CKRecordZoneID*)zoneID error: (NSError * __autoreleasing *) error { 166 return [self allWhere:@{@"ckzone":zoneID.zoneName} error:error]; 167 } 168 169 + (bool) deleteAll:(CKRecordZoneID*) zoneID error: (NSError * __autoreleasing *) error { 170 bool ok = [CKKSSQLDatabaseObject deleteFromTable:[self sqlTable] where: @{@"ckzone":zoneID.zoneName} connection:nil error: error]; 171 172 if(ok) { 173 secdebug("ckksitem", "Deleted all %@", self); 174 } else { 175 secdebug("ckksitem", "Couldn't delete all %@: %@", self, error ? *error : @"unknown"); 176 } 177 return ok; 178 } 179 180 #pragma mark - CKKSSQLDatabaseObject methods 181 182 + (NSString*) sqlTable { 183 return @"currentkeys"; 184 } 185 186 + (NSArray<NSString*>*) sqlColumns { 187 return @[@"keyclass", @"currentKeyUUID", @"ckzone", @"ckrecord"]; 188 } 189 190 - (NSDictionary<NSString*,NSString*>*) whereClauseToFindSelf { 191 return @{@"keyclass": self.keyclass, @"ckzone":self.zoneID.zoneName}; 192 } 193 194 - (NSDictionary<NSString*,NSString*>*) sqlValues { 195 return @{@"keyclass": self.keyclass, 196 @"currentKeyUUID": CKKSNilToNSNull(self.currentKeyUUID), 197 @"ckzone": CKKSNilToNSNull(self.zoneID.zoneName), 198 @"ckrecord": CKKSNilToNSNull([self.encodedCKRecord base64EncodedStringWithOptions:0]), 199 }; 200 } 201 202 + (instancetype)fromDatabaseRow:(NSDictionary<NSString*, CKKSSQLResult*>*)row { 203 return [[CKKSCurrentKeyPointer alloc] initForClass:(CKKSKeyClass*)row[@"keyclass"].asString 204 currentKeyUUID:row[@"currentKeyUUID"].asString 205 zoneID:[[CKRecordZoneID alloc] initWithZoneName:row[@"ckzone"].asString ownerName:CKCurrentUserDefaultName] 206 encodedCKRecord:row[@"ckrecord"].asBase64DecodedData]; 207 } 208 209 + (BOOL)intransactionRecordChanged:(CKRecord*)record 210 resync:(BOOL)resync 211 flagHandler:(id<OctagonStateFlagHandler>)flagHandler 212 error:(NSError**)error 213 { 214 // Pull out the old CKP, if it exists 215 NSError* ckperror = nil; 216 CKKSCurrentKeyPointer* oldckp = [CKKSCurrentKeyPointer tryFromDatabase:((CKKSKeyClass*)record.recordID.recordName) zoneID:record.recordID.zoneID error:&ckperror]; 217 if(ckperror) { 218 ckkserror("ckkskey", record.recordID.zoneID, "error loading ckp: %@", ckperror); 219 } 220 221 if(resync) { 222 if(!oldckp) { 223 ckkserror("ckksresync", record.recordID.zoneID, "BUG: No current key pointer matching resynced CloudKit record: %@", record); 224 } else if(![oldckp matchesCKRecord:record]) { 225 ckkserror("ckksresync", record.recordID.zoneID, "BUG: Local current key pointer doesn't match resynced CloudKit record: %@ %@", oldckp, record); 226 } else { 227 ckksnotice("ckksresync", record.recordID.zoneID, "Current key pointer has 'changed', but it matches our local copy: %@", record); 228 } 229 } 230 231 NSError* localerror = nil; 232 CKKSCurrentKeyPointer* currentkey = [[CKKSCurrentKeyPointer alloc] initWithCKRecord:record]; 233 234 bool saved = [currentkey saveToDatabase:&localerror]; 235 if(!saved || localerror != nil) { 236 ckkserror("ckkskey", record.recordID.zoneID, "Couldn't save current key pointer to database: %@: %@", currentkey, localerror); 237 ckksinfo("ckkskey", record.recordID.zoneID, "CKRecord was %@", record); 238 239 if(error) { 240 *error = localerror; 241 } 242 return NO; 243 } 244 245 if([oldckp matchesCKRecord:record]) { 246 ckksnotice("ckkskey", record.recordID.zoneID, "Current key pointer modification doesn't change anything interesting; skipping reprocess: %@", record); 247 } else { 248 // We've saved a new key in the database; trigger a rekey operation. 249 [flagHandler _onqueueHandleFlag:CKKSFlagKeyStateProcessRequested]; 250 } 251 252 return YES; 253 } 254 255 @end 256 257 @implementation CKKSCurrentKeySet 258 -(instancetype)initForZoneName:(NSString*)zoneName { 259 if((self = [super init])) { 260 _viewName = zoneName; 261 } 262 263 return self; 264 } 265 266 + (CKKSCurrentKeySet*)loadForZone:(CKRecordZoneID*)zoneID 267 { 268 CKKSCurrentKeySet* set = [[CKKSCurrentKeySet alloc] initForZoneName:zoneID.zoneName]; 269 NSError* error = nil; 270 271 set.currentTLKPointer = [CKKSCurrentKeyPointer tryFromDatabase: SecCKKSKeyClassTLK zoneID:zoneID error:&error]; 272 set.currentClassAPointer = [CKKSCurrentKeyPointer tryFromDatabase: SecCKKSKeyClassA zoneID:zoneID error:&error]; 273 set.currentClassCPointer = [CKKSCurrentKeyPointer tryFromDatabase: SecCKKSKeyClassC zoneID:zoneID error:&error]; 274 275 set.tlk = set.currentTLKPointer.currentKeyUUID ? [CKKSKey tryFromDatabase:set.currentTLKPointer.currentKeyUUID zoneID:zoneID error:&error] : nil; 276 set.classA = set.currentClassAPointer.currentKeyUUID ? [CKKSKey tryFromDatabase:set.currentClassAPointer.currentKeyUUID zoneID:zoneID error:&error] : nil; 277 set.classC = set.currentClassCPointer.currentKeyUUID ? [CKKSKey tryFromDatabase:set.currentClassCPointer.currentKeyUUID zoneID:zoneID error:&error] : nil; 278 279 set.tlkShares = [CKKSTLKShareRecord allForUUID:set.currentTLKPointer.currentKeyUUID zoneID:zoneID error:&error]; 280 set.pendingTLKShares = nil; 281 282 set.proposed = NO; 283 284 set.error = error; 285 286 return set; 287 } 288 289 -(NSString*)description { 290 if(self.error) { 291 return [NSString stringWithFormat:@"<CKKSCurrentKeySet(%@): %@:%@ %@:%@ %@:%@ new:%d %@>", 292 self.viewName, 293 self.currentTLKPointer.currentKeyUUID, self.tlk, 294 self.currentClassAPointer.currentKeyUUID, self.classA, 295 self.currentClassCPointer.currentKeyUUID, self.classC, 296 self.proposed, 297 self.error]; 298 299 } else { 300 return [NSString stringWithFormat:@"<CKKSCurrentKeySet(%@): %@:%@ %@:%@ %@:%@ new:%d>", 301 self.viewName, 302 self.currentTLKPointer.currentKeyUUID, self.tlk, 303 self.currentClassAPointer.currentKeyUUID, self.classA, 304 self.currentClassCPointer.currentKeyUUID, self.classC, 305 self.proposed]; 306 } 307 } 308 - (instancetype)copyWithZone:(NSZone*)zone { 309 CKKSCurrentKeySet* copy = [[[self class] alloc] init]; 310 copy.currentTLKPointer = [self.currentTLKPointer copyWithZone:zone]; 311 copy.currentClassAPointer = [self.currentClassAPointer copyWithZone:zone]; 312 copy.currentClassCPointer = [self.currentClassCPointer copyWithZone:zone]; 313 copy.tlk = [self.tlk copyWithZone:zone]; 314 copy.classA = [self.classA copyWithZone:zone]; 315 copy.classC = [self.classC copyWithZone:zone]; 316 copy.proposed = self.proposed; 317 318 copy.error = [self.error copyWithZone:zone]; 319 return copy; 320 } 321 322 - (CKKSKeychainBackedKeySet* _Nullable)asKeychainBackedSet:(NSError**)error 323 { 324 if(!self.tlk.keycore || 325 !self.classA.keycore || 326 !self.classC.keycore) { 327 if(error) { 328 *error = [NSError errorWithDomain:CKKSErrorDomain 329 code:CKKSKeysMissing 330 description:@"unable to make keychain backed set; key is missing"]; 331 } 332 return nil; 333 } 334 335 return [[CKKSKeychainBackedKeySet alloc] initWithTLK:self.tlk.keycore 336 classA:self.classA.keycore 337 classC:self.classC.keycore 338 newUpload:self.proposed]; 339 } 340 @end 341 342 #endif // OCTAGON