/ keychain / ckks / CKKSCurrentKeyPointer.m
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