2 * Copyright (c) 2016 Apple Inc. All Rights Reserved.
4 * @APPLE_LICENSE_HEADER_START@
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
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.
21 * @APPLE_LICENSE_HEADER_END@
24 #include <AssertMacros.h>
26 #import <Foundation/Foundation.h>
30 #include <utilities/SecDb.h>
31 #include <securityd/SecDbItem.h>
32 #include <securityd/SecItemSchema.h>
36 #import <CloudKit/CloudKit.h>
37 #import <CloudKit/CloudKit_Private.h>
39 @implementation CKKSItem
41 - (instancetype) initWithCKRecord: (CKRecord*) record {
42 if(self = [super initWithCKRecord: record]) {
47 - (instancetype) initCopyingCKKSItem: (CKKSItem*) item {
48 if(self = [super initWithCKRecordType: item.ckRecordType encodedCKRecord:item.encodedCKRecord zoneID:item.zoneID]) {
50 _parentKeyUUID = item.parentKeyUUID;
51 _generationCount = item.generationCount;
52 _encitem = item.encitem;
53 _wrappedkey = item.wrappedkey;
54 _encver = item.encver;
56 _plaintextPCSServiceIdentifier = item.plaintextPCSServiceIdentifier;
57 _plaintextPCSPublicKey = item.plaintextPCSPublicKey;
58 _plaintextPCSPublicIdentity = item.plaintextPCSPublicIdentity;
63 - (instancetype) initWithUUID: (NSString*) uuid
64 parentKeyUUID: (NSString*) parentKeyUUID
65 zoneID: (CKRecordZoneID*) zoneID
67 return [self initWithUUID:uuid
68 parentKeyUUID:parentKeyUUID
74 encver:CKKSItemEncryptionVersionNone];
77 - (instancetype) initWithUUID: (NSString*) uuid
78 parentKeyUUID: (NSString*) parentKeyUUID
79 zoneID: (CKRecordZoneID*) zoneID
80 encItem: (NSData*) encitem
81 wrappedkey: (CKKSWrappedAESSIVKey*) wrappedkey
82 generationCount: (NSUInteger) genCount
83 encver: (NSUInteger) encver
85 return [self initWithUUID:uuid
86 parentKeyUUID:parentKeyUUID
91 generationCount:genCount
95 - (instancetype) initWithUUID: (NSString*) uuid
96 parentKeyUUID: (NSString*) parentKeyUUID
97 zoneID: (CKRecordZoneID*)zoneID
98 encodedCKRecord: (NSData*) encodedrecord
99 encItem: (NSData*) encitem
100 wrappedkey: (CKKSWrappedAESSIVKey*) wrappedkey
101 generationCount: (NSUInteger) genCount
102 encver: (NSUInteger) encver
104 return [self initWithUUID:uuid
105 parentKeyUUID:parentKeyUUID
107 encodedCKRecord:encodedrecord
109 wrappedkey:wrappedkey
110 generationCount:genCount
112 plaintextPCSServiceIdentifier:nil
113 plaintextPCSPublicKey:nil
114 plaintextPCSPublicIdentity:nil];
117 - (instancetype) initWithUUID: (NSString*) uuid
118 parentKeyUUID: (NSString*) parentKeyUUID
119 zoneID: (CKRecordZoneID*)zoneID
120 encodedCKRecord: (NSData*) encodedrecord
121 encItem: (NSData*) encitem
122 wrappedkey: (CKKSWrappedAESSIVKey*) wrappedkey
123 generationCount: (NSUInteger) genCount
124 encver: (NSUInteger) encver
125 plaintextPCSServiceIdentifier: (NSNumber*) pcsServiceIdentifier
126 plaintextPCSPublicKey: (NSData*) pcsPublicKey
127 plaintextPCSPublicIdentity: (NSData*) pcsPublicIdentity
129 if(self = [super initWithCKRecordType: SecCKRecordItemType encodedCKRecord:encodedrecord zoneID:zoneID]) {
131 _parentKeyUUID = parentKeyUUID;
132 _generationCount = genCount;
133 self.encitem = encitem;
134 _wrappedkey = wrappedkey;
137 _plaintextPCSServiceIdentifier = pcsServiceIdentifier;
138 _plaintextPCSPublicKey = pcsPublicKey;
139 _plaintextPCSPublicIdentity = pcsPublicIdentity;
145 - (BOOL)isEqual: (id) object {
146 if(![object isKindOfClass:[CKKSItem class]]) {
150 CKKSItem* obj = (CKKSItem*) object;
152 return ([self.uuid isEqual: obj.uuid] &&
153 [self.parentKeyUUID isEqual: obj.parentKeyUUID] &&
154 [self.zoneID isEqual: obj.zoneID] &&
155 ((self.encitem == nil && obj.encitem == nil) || ([self.encitem isEqual: obj.encitem])) &&
156 [self.wrappedkey isEqual: obj.wrappedkey] &&
157 self.generationCount == obj.generationCount &&
158 self.encver == obj.encver &&
162 #pragma mark - CKRecord handling
164 - (NSString*) CKRecordName {
168 - (void) setFromCKRecord: (CKRecord*) record {
169 if(![record.recordType isEqual: SecCKRecordItemType]) {
171 exceptionWithName:@"WrongCKRecordTypeException"
172 reason:[NSString stringWithFormat: @"CKRecordType (%@) was not %@", record.recordType, SecCKRecordItemType]
176 [self setStoredCKRecord:record];
178 _uuid = [[record recordID] recordName];
179 self.parentKeyUUID = [record[SecCKRecordParentKeyRefKey] recordID].recordName;
180 self.encitem = record[SecCKRecordDataKey];
181 self.wrappedkey = [[CKKSWrappedAESSIVKey alloc] initWithBase64: record[SecCKRecordWrappedKeyKey]];
182 self.generationCount = [record[SecCKRecordGenerationCountKey] unsignedIntegerValue];
183 self.encver = [record[SecCKRecordEncryptionVersionKey] unsignedIntegerValue];
185 self.plaintextPCSServiceIdentifier = record[SecCKRecordPCSServiceIdentifier];
186 self.plaintextPCSPublicKey = record[SecCKRecordPCSPublicKey];
187 self.plaintextPCSPublicIdentity = record[SecCKRecordPCSPublicIdentity];
190 + (void)setOSVersionInRecord: (CKRecord*) record {
192 // Use complicated macro magic to get the string value passed in as preprocessor define PLATFORM.
193 #define PLATFORM_VALUE(f) #f
194 #define PLATFORM_OBJCSTR(f) @PLATFORM_VALUE(f)
195 NSString* platform = (PLATFORM_OBJCSTR(PLATFORM));
196 #undef PLATFORM_OBJCSTR
197 #undef PLATFORM_VALUE
199 NSString* platform = "unknown";
200 #warning No PLATFORM defined; why?
202 NSString* osversion = [[NSProcessInfo processInfo]operatingSystemVersionString];
204 // subtly improve osversion (but it's okay if that does nothing)
205 NSString* finalversion = [platform stringByAppendingString: [osversion stringByReplacingOccurrencesOfString:@"Version" withString:@""]];
206 record[SecCKRecordVersionKey] = finalversion;
209 - (CKRecord*) updateCKRecord: (CKRecord*) record zoneID: (CKRecordZoneID*) zoneID {
210 if(![record.recordType isEqual: SecCKRecordItemType]) {
212 exceptionWithName:@"WrongCKRecordTypeException"
213 reason:[NSString stringWithFormat: @"CKRecordType (%@) was not %@", record.recordType, SecCKRecordItemType]
217 // Items must have a wrapping key.
218 record[SecCKRecordParentKeyRefKey] = [[CKReference alloc] initWithRecordID: [[CKRecordID alloc] initWithRecordName: self.parentKeyUUID zoneID: zoneID] action: CKReferenceActionValidate];
220 [CKKSItem setOSVersionInRecord: record];
222 record[SecCKRecordDataKey] = self.encitem;
223 record[SecCKRecordWrappedKeyKey] = [self.wrappedkey base64WrappedKey];
224 record[SecCKRecordGenerationCountKey] = [NSNumber numberWithInteger:self.generationCount];
225 // TODO: if the record's generation count is already higher than ours, that's a problem.
226 record[SecCKRecordEncryptionVersionKey] = [NSNumber numberWithInteger:self.encver];
228 // Add unencrypted fields
229 record[SecCKRecordPCSServiceIdentifier] = self.plaintextPCSServiceIdentifier;
230 record[SecCKRecordPCSPublicKey] = self.plaintextPCSPublicKey;
231 record[SecCKRecordPCSPublicIdentity] = self.plaintextPCSPublicIdentity;
237 - (bool) matchesCKRecord: (CKRecord*) record {
238 if(![record.recordType isEqual: SecCKRecordItemType]) {
242 // We only really care about the data, the wrapped key, the generation count, and the parent key.
243 // Note that since all of those things are included as authenticated data into the AES-SIV ciphertext, we could just
244 // compare that. However, check 'em all.
245 if(![record.recordID.recordName isEqualToString: self.uuid]) {
246 secinfo("ckksitem", "UUID does not match");
250 if(![[record[SecCKRecordParentKeyRefKey] recordID].recordName isEqualToString: self.parentKeyUUID]) {
251 secinfo("ckksitem", "wrapping key reference does not match");
255 if(![record[SecCKRecordGenerationCountKey] isEqual: [NSNumber numberWithInteger:self.generationCount]]) {
256 secinfo("ckksitem", "SecCKRecordGenerationCountKey does not match");
260 if(![record[SecCKRecordWrappedKeyKey] isEqual: [self.wrappedkey base64WrappedKey]]) {
261 secinfo("ckksitem", "SecCKRecordWrappedKeyKey does not match");
265 if(![record[SecCKRecordDataKey] isEqual: self.encitem]) {
266 secinfo("ckksitem", "SecCKRecordDataKey does not match");
270 // Compare plaintext records, too
271 // Why is obj-c nullable equality so difficult?
272 if(!((record[SecCKRecordPCSServiceIdentifier] == nil && self.plaintextPCSServiceIdentifier == nil) ||
273 [record[SecCKRecordPCSServiceIdentifier] isEqual: self.plaintextPCSServiceIdentifier])) {
274 secinfo("ckksitem", "SecCKRecordPCSServiceIdentifier does not match");
278 if(!((record[SecCKRecordPCSPublicKey] == nil && self.plaintextPCSPublicKey == nil) ||
279 [record[SecCKRecordPCSPublicKey] isEqual: self.plaintextPCSPublicKey])) {
280 secinfo("ckksitem", "SecCKRecordPCSPublicKey does not match");
284 if(!((record[SecCKRecordPCSPublicIdentity] == nil && self.plaintextPCSPublicIdentity == nil) ||
285 [record[SecCKRecordPCSPublicIdentity] isEqual: self.plaintextPCSPublicIdentity])) {
286 secinfo("ckksitem", "SecCKRecordPCSPublicIdentity does not match");
293 // Generates the list of 'authenticated data' to go along with this item, and optionally adds in unknown, future fields received from CloudKit
294 - (NSDictionary<NSString*, NSData*>*)makeAuthenticatedDataDictionaryUpdatingCKKSItem:(CKKSItem*) olditem encryptionVersion:(SecCKKSItemEncryptionVersion)encversion {
296 case CKKSItemEncryptionVersion1:
297 return [self makeAuthenticatedDataDictionaryUpdatingCKKSItemEncVer1];
298 case CKKSItemEncryptionVersion2:
299 return [self makeAuthenticatedDataDictionaryUpdatingCKKSItemEncVer2:olditem];
302 exceptionWithName:@"WrongEncryptionVersionException"
303 reason:[NSString stringWithFormat: @"%d is not a known encryption version", (int)encversion]
308 - (NSDictionary<NSString*, NSData*>*)makeAuthenticatedDataDictionaryUpdatingCKKSItemEncVer1 {
309 NSMutableDictionary<NSString*, NSData*>* authenticatedData = [[NSMutableDictionary alloc] init];
311 authenticatedData[@"UUID"] = [self.uuid dataUsingEncoding: NSUTF8StringEncoding];
312 authenticatedData[SecCKRecordWrappedKeyKey] = [self.parentKeyUUID dataUsingEncoding: NSUTF8StringEncoding];
314 uint64_t genCount64 = OSSwapHostToLittleConstInt64(self.generationCount);
315 authenticatedData[SecCKRecordGenerationCountKey] = [NSData dataWithBytes:&genCount64 length:sizeof(genCount64)];
317 uint64_t encver = OSSwapHostToLittleConstInt64((uint64_t)self.encver);
318 authenticatedData[SecCKRecordEncryptionVersionKey] = [NSData dataWithBytes:&encver length:sizeof(encver)];
320 // In v1, don't authenticate the plaintext PCS fields
321 authenticatedData[SecCKRecordPCSServiceIdentifier] = nil;
322 authenticatedData[SecCKRecordPCSPublicKey] = nil;
323 authenticatedData[SecCKRecordPCSPublicIdentity] = nil;
325 return authenticatedData;
328 - (NSDictionary<NSString*, NSData*>*)makeAuthenticatedDataDictionaryUpdatingCKKSItemEncVer2:(CKKSItem*) olditem {
329 NSMutableDictionary<NSString*, NSData*>* authenticatedData = [[NSMutableDictionary alloc] init];
331 authenticatedData[@"UUID"] = [self.uuid dataUsingEncoding: NSUTF8StringEncoding];
332 authenticatedData[SecCKRecordWrappedKeyKey] = [self.parentKeyUUID dataUsingEncoding: NSUTF8StringEncoding];
334 uint64_t genCount64 = OSSwapHostToLittleConstInt64(self.generationCount);
335 authenticatedData[SecCKRecordGenerationCountKey] = [NSData dataWithBytes:&genCount64 length:sizeof(genCount64)];
337 uint64_t encver = OSSwapHostToLittleConstInt64((uint64_t)self.encver);
338 authenticatedData[SecCKRecordEncryptionVersionKey] = [NSData dataWithBytes:&encver length:sizeof(encver)];
340 // v2 authenticates the PCS fields too
341 if(self.plaintextPCSServiceIdentifier) {
342 uint64_t pcsServiceIdentifier = OSSwapHostToLittleConstInt64([self.plaintextPCSServiceIdentifier unsignedLongValue]);
343 authenticatedData[SecCKRecordPCSServiceIdentifier] = [NSData dataWithBytes:&pcsServiceIdentifier length:sizeof(pcsServiceIdentifier)];
345 authenticatedData[SecCKRecordPCSPublicKey] = self.plaintextPCSPublicKey;
346 authenticatedData[SecCKRecordPCSPublicIdentity] = self.plaintextPCSPublicIdentity;
348 // Iterate through the fields in the old CKKSItem. If we don't recognize any of them, add them to the authenticated data.
350 CKRecord* record = olditem.storedCKRecord;
352 for(NSString* key in record.allKeys) {
353 if([key isEqualToString:@"UUID"] ||
354 [key isEqualToString:SecCKRecordVersionKey] ||
355 [key isEqualToString:SecCKRecordDataKey] ||
356 [key isEqualToString:SecCKRecordWrappedKeyKey] ||
357 [key isEqualToString:SecCKRecordGenerationCountKey] ||
358 [key isEqualToString:SecCKRecordEncryptionVersionKey] ||
359 [key isEqualToString:SecCKRecordPCSServiceIdentifier] ||
360 [key isEqualToString:SecCKRecordPCSPublicKey] ||
361 [key isEqualToString:SecCKRecordPCSPublicIdentity]) {
362 // This version of CKKS knows about this data field. Ignore them with prejudice.
366 if([key hasPrefix:@"server_"]) {
367 // Ignore all fields prefixed by "server_"
371 id obj = record[key];
373 // Skip CKReferences, NSArray, CLLocation, and CKAsset.
374 if([obj isKindOfClass: [NSString class]]) {
376 authenticatedData[key] = [obj dataUsingEncoding: NSUTF8StringEncoding];
377 } else if([obj isKindOfClass: [NSData class]]) {
379 authenticatedData[key] = [obj copy];
380 } else if([obj isKindOfClass:[NSDate class]]) {
382 NSISO8601DateFormatter *formatter = [[NSISO8601DateFormatter alloc] init];
383 NSString* str = [formatter stringForObjectValue: obj];
385 authenticatedData[key] = [str dataUsingEncoding: NSUTF8StringEncoding];
386 } else if([obj isKindOfClass: [NSNumber class]]) {
388 uint64_t n64 = OSSwapHostToLittleConstInt64([obj unsignedLongLongValue]);
389 authenticatedData[key] = [NSData dataWithBytes:&n64 length:sizeof(n64)];
396 // TODO: add unauth'ed field name here
398 return authenticatedData;
401 #pragma mark - Utility
403 - (NSString*)description {
404 return [NSString stringWithFormat: @"<%@: %@>", NSStringFromClass([self class]), self.uuid];
407 - (NSString*)debugDescription {
408 return [NSString stringWithFormat: @"<%@: %@ %p>", NSStringFromClass([self class]), self.uuid, self];
411 - (instancetype)copyWithZone:(NSZone *)zone {
412 CKKSItem *itemCopy = [super copyWithZone:zone];
413 itemCopy->_uuid = _uuid;
414 itemCopy->_parentKeyUUID = _parentKeyUUID;
415 itemCopy->_encitem = _encitem;
416 itemCopy->_wrappedkey = _wrappedkey;
417 itemCopy->_generationCount = _generationCount;
418 itemCopy->_encver = _encver;
422 #pragma mark - Getters/Setters
424 - (NSString*) base64Item {
425 return [self.encitem base64EncodedStringWithOptions:0];
428 - (void) setBase64Item: (NSString*) base64Item {
429 _encitem = [[NSData alloc] initWithBase64EncodedString: base64Item options:0];
432 #pragma mark - CKKSSQLDatabaseObject helpers
434 // Note that CKKSItems are not intended to be saved directly, and so CKKSItem does not implement sqlTable.
435 // You must subclass CKKSItem to have this work correctly, although you can call back up into this class to use these if you like.
437 + (NSArray<NSString*>*)sqlColumns {
438 return @[@"UUID", @"parentKeyUUID", @"ckzone", @"encitem", @"wrappedkey", @"gencount", @"encver", @"ckrecord",
439 @"pcss", @"pcsk", @"pcsi"];
442 - (NSDictionary<NSString*,NSString*>*)whereClauseToFindSelf {
443 return @{@"UUID": self.uuid, @"ckzone":self.zoneID.zoneName};
446 - (NSDictionary<NSString*,NSString*>*)sqlValues {
447 return @{@"UUID": self.uuid,
448 @"parentKeyUUID": self.parentKeyUUID,
449 @"ckzone": CKKSNilToNSNull(self.zoneID.zoneName),
450 @"encitem": self.base64encitem,
451 @"wrappedkey": [self.wrappedkey base64WrappedKey],
452 @"gencount": [NSNumber numberWithInteger:self.generationCount],
453 @"encver": [NSNumber numberWithInteger:self.encver],
454 @"ckrecord": CKKSNilToNSNull([self.encodedCKRecord base64EncodedStringWithOptions:0]),
455 @"pcss": CKKSNilToNSNull(self.plaintextPCSServiceIdentifier),
456 @"pcsk": CKKSNilToNSNull([self.plaintextPCSPublicKey base64EncodedStringWithOptions:0]),
457 @"pcsi": CKKSNilToNSNull([self.plaintextPCSPublicIdentity base64EncodedStringWithOptions:0])};
460 + (instancetype)fromDatabaseRow: (NSDictionary*) row {
461 return [[CKKSItem alloc] initWithUUID:row[@"UUID"]
462 parentKeyUUID:row[@"parentKeyUUID"]
463 zoneID:[[CKRecordZoneID alloc] initWithZoneName: row[@"ckzone"] ownerName:CKCurrentUserDefaultName]
464 encodedCKRecord:CKKSUnbase64NullableString(row[@"ckrecord"])
465 encItem:CKKSUnbase64NullableString(row[@"encitem"])
466 wrappedkey:CKKSIsNull(row[@"wrappedkey"]) ? nil : [[CKKSWrappedAESSIVKey alloc] initWithBase64: row[@"wrappedkey"]]
467 generationCount:[row[@"gencount"] integerValue]
468 encver:[row[@"encver"] integerValue]
469 plaintextPCSServiceIdentifier:CKKSIsNull(row[@"pcss"]) ? nil : [NSNumber numberWithInteger: [row[@"pcss"] integerValue]]
470 plaintextPCSPublicKey:CKKSUnbase64NullableString(row[@"pcsk"])
471 plaintextPCSPublicIdentity:CKKSUnbase64NullableString(row[@"pcsi"])
477 #pragma mark - CK-Aware Database Helpers
479 @implementation CKKSSQLDatabaseObject (CKKSZoneExtras)
481 + (NSArray<NSString*>*) allUUIDs: (NSError * __autoreleasing *) error {
482 __block NSMutableArray* uuids = [[NSMutableArray alloc] init];
484 [CKKSSQLDatabaseObject queryDatabaseTable: [self sqlTable]
490 processRow: ^(NSDictionary* row) {
491 [uuids addObject: row[@"UUID"]];
497 + (NSArray*) all:(CKRecordZoneID*) zoneID error: (NSError * __autoreleasing *) error {
498 return [self allWhere: @{@"ckzone": CKKSNilToNSNull(zoneID.zoneName)} error:error];
501 + (bool) deleteAll:(CKRecordZoneID*) zoneID error: (NSError * __autoreleasing *) error {
502 bool ok = [CKKSSQLDatabaseObject deleteFromTable:[self sqlTable] where: @{@"ckzone":CKKSNilToNSNull(zoneID.zoneName)} connection:nil error: error];
505 secdebug("ckksitem", "Deleted all %@", self);
507 secdebug("ckksitem", "Couldn't delete all %@: %@", self, error ? *error : @"unknown");