]> git.saurik.com Git - apple/security.git/blob - keychain/ckks/CKKSOutgoingQueueEntry.m
Security-59754.41.1.tar.gz
[apple/security.git] / keychain / ckks / CKKSOutgoingQueueEntry.m
1 /*
2 * Copyright (c) 2016 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 #include <AssertMacros.h>
25
26 #import <Foundation/Foundation.h>
27
28 #import "CKKSKeychainView.h"
29
30 #include <Security/SecItemPriv.h>
31
32 #include <utilities/SecDb.h>
33 #include "keychain/securityd/SecDbItem.h"
34 #include "keychain/securityd/SecItemSchema.h"
35
36 #if OCTAGON
37
38 #import <CloudKit/CloudKit.h>
39 #import "CKKSOutgoingQueueEntry.h"
40 #import "CKKSItemEncrypter.h"
41 #import "CKKSKey.h"
42 #import "keychain/ckks/CloudKitCategories.h"
43 #import "keychain/categories/NSError+UsefulConstructors.h"
44
45
46 @implementation CKKSOutgoingQueueEntry
47
48 - (NSString*)description {
49 return [NSString stringWithFormat: @"<%@(%@): %@ %@ (%@)>",
50 NSStringFromClass([self class]),
51 self.item.zoneID.zoneName,
52 self.action,
53 self.item.uuid,
54 self.state];
55 }
56
57 - (instancetype) initWithCKKSItem:(CKKSItem*) item
58 action: (NSString*) action
59 state: (NSString*) state
60 waitUntil: (NSDate*) waitUntil
61 accessGroup: (NSString*) accessgroup
62 {
63 if((self = [super init])) {
64 _item = item;
65 _action = action;
66 _state = state;
67 _accessgroup = accessgroup;
68 _waitUntil = waitUntil;
69 }
70
71 return self;
72 }
73
74 - (BOOL)isEqual: (id) object {
75 if(![object isKindOfClass:[CKKSOutgoingQueueEntry class]]) {
76 return NO;
77 }
78
79 CKKSOutgoingQueueEntry* obj = (CKKSOutgoingQueueEntry*) object;
80
81 return ([self.item isEqual: obj.item] &&
82 [self.action isEqual: obj.action] &&
83 [self.state isEqual: obj.state] &&
84 ((self.waitUntil == nil && obj.waitUntil == nil) || (fabs([self.waitUntil timeIntervalSinceDate: obj.waitUntil]) < 1)) &&
85 [self.accessgroup isEqual: obj.accessgroup] &&
86 true) ? YES : NO;
87 }
88
89
90 + (CKKSKey*)keyForItem:(SecDbItemRef)item zoneID:(CKRecordZoneID*)zoneID error:(NSError * __autoreleasing *)error
91 {
92 if(!item) {
93 ckkserror("ckks-key", zoneID, "Cannot select a key for no item!");
94 if(error) {
95 *error = [NSError errorWithDomain:CKKSErrorDomain
96 code:CKKSErrorUnexpectedNil
97 description:@"can't pick a key class for an empty item"];
98 }
99 return nil;
100 }
101
102 CKKSKeyClass* class = nil;
103
104 NSString* protection = (__bridge NSString*)SecDbItemGetCachedValueWithName(item, kSecAttrAccessible);
105 if([protection isEqualToString: (__bridge NSString*)kSecAttrAccessibleWhenUnlocked]) {
106 class = SecCKKSKeyClassA;
107 } else if([protection isEqualToString: (__bridge NSString*)kSecAttrAccessibleAlwaysPrivate] ||
108 [protection isEqualToString: (__bridge NSString*)kSecAttrAccessibleAfterFirstUnlock]) {
109 class = SecCKKSKeyClassC;
110 } else {
111 NSError* localError = [NSError errorWithDomain:CKKSErrorDomain
112 code:CKKSInvalidKeyClass
113 description:[NSString stringWithFormat:@"can't pick key class for protection %@", protection]];
114 ckkserror("ckks-key", zoneID, "can't pick key class: %@ %@", localError, item);
115 if(error) {
116 *error = localError;
117 }
118
119 return nil;
120 }
121
122 NSError* currentKeyError = nil;
123 CKKSKey* key = [CKKSKey currentKeyForClass:class zoneID:zoneID error:&currentKeyError];
124 if(!key || currentKeyError) {
125 ckkserror("ckks-key", zoneID, "Couldn't find current key for %@: %@", class, currentKeyError);
126
127 if(error) {
128 *error = currentKeyError;
129 }
130 return nil;
131 }
132
133 // and make sure it's unwrapped.
134 NSError* loadedError = nil;
135 if(![key ensureKeyLoaded:&loadedError]) {
136 ckkserror("ckks-key", zoneID, "Couldn't load key(%@): %@", key, loadedError);
137 if(error) {
138 *error = loadedError;
139 }
140 return nil;
141 }
142
143 return key;
144 }
145
146 + (instancetype)withItem:(SecDbItemRef)item
147 action:(NSString*)action
148 zoneID:(CKRecordZoneID*)zoneID
149 error:(NSError * __autoreleasing *)error
150 {
151 CFErrorRef cferror = NULL;
152 CKKSKey* key = nil;
153 NSString* uuid = nil;
154 NSString* accessgroup = nil;
155
156 NSInteger newGenerationCount = -1;
157
158 NSMutableDictionary* objd = nil;
159
160 ckkserror("ckksitem", zoneID, "Creating a (%@) outgoing queue entry for: %@", action, item);
161
162 NSError* keyError = nil;
163 key = [self keyForItem:item
164 zoneID:zoneID
165 error:&keyError];
166 if(!key || keyError) {
167 NSError* localerror = [NSError errorWithDomain:CKKSErrorDomain code:keyError.code description:@"No key for item" underlying:keyError];
168 ckkserror("ckksitem", zoneID, "no key for item: %@ %@", localerror, item);
169 if(error) {
170 *error = localerror;
171 }
172 return nil;
173 }
174
175 objd = (__bridge_transfer NSMutableDictionary*) SecDbItemCopyPListWithMask(item, kSecDbSyncFlag, &cferror);
176 if(!objd) {
177 NSError* localerror = [NSError errorWithDomain:CKKSErrorDomain code:CFErrorGetCode(cferror) description:@"Couldn't create object plist" underlying:(__bridge_transfer NSError*)cferror];
178 ckkserror("ckksitem", zoneID, "no plist: %@ %@", localerror, item);
179 if(error) {
180 *error = localerror;
181 }
182 return nil;
183 }
184
185 // Object classes aren't in the item plist, set them specifically
186 [objd setObject: (__bridge NSString*) item->class->name forKey: (__bridge NSString*) kSecClass];
187
188 uuid = (__bridge_transfer NSString*) CFRetainSafe(SecDbItemGetValue(item, &v10itemuuid, &cferror));
189 if(!uuid || cferror) {
190 NSError* localerror = [NSError errorWithDomain:CKKSErrorDomain code:CKKSNoUUIDOnItem description:@"No UUID for item" underlying:(__bridge_transfer NSError*)cferror];
191 ckkserror("ckksitem", zoneID, "No UUID for item: %@ %@", localerror, item);
192 if(error) {
193 *error = localerror;
194 }
195 return nil;
196 }
197 if([uuid isKindOfClass:[NSNull class]]) {
198 NSError* localerror = [NSError errorWithDomain:CKKSErrorDomain code:CKKSNoUUIDOnItem description:@"UUID not found in object" underlying:nil];
199 ckkserror("ckksitem", zoneID, "couldn't fetch UUID: %@ %@", localerror, item);
200 if(error) {
201 *error = localerror;
202 }
203 return nil;
204 }
205
206 accessgroup = (__bridge_transfer NSString*) CFRetainSafe(SecDbItemGetValue(item, &v6agrp, &cferror));
207 if(!accessgroup || cferror) {
208 NSError* localerror = [NSError errorWithDomain:CKKSErrorDomain code:CFErrorGetCode(cferror) description:@"accessgroup not found in object" underlying:(__bridge_transfer NSError*)cferror];
209 ckkserror("ckksitem", zoneID, "couldn't fetch access group from item: %@ %@", localerror, item);
210 if(error) {
211 *error = localerror;
212 }
213 return nil;
214 }
215 if([accessgroup isKindOfClass:[NSNull class]]) {
216 // That's okay; this is only used for rate limiting.
217 ckkserror("ckksitem", zoneID, "couldn't fetch accessgroup: %@", item);
218 accessgroup = @"no-group";
219 }
220
221 CKKSMirrorEntry* ckme = [CKKSMirrorEntry tryFromDatabase:uuid zoneID:zoneID error:error];
222
223 // The action this change should be depends on any existing pending action, if any
224 // Particularly, we need to coalesce (existing action, new action) to:
225 // (add, modify) => add
226 // (add, delete) => no-op
227 // (delete, add) => modify
228 NSString* actualAction = action;
229
230 NSError* fetchError = nil;
231 CKKSOutgoingQueueEntry* existingOQE = [CKKSOutgoingQueueEntry tryFromDatabase:uuid state:SecCKKSStateNew zoneID:zoneID error:&fetchError];
232 if(existingOQE) {
233 if([existingOQE.action isEqual: SecCKKSActionAdd]) {
234 if([action isEqual:SecCKKSActionModify]) {
235 actualAction = SecCKKSActionAdd;
236 } else if([action isEqual:SecCKKSActionDelete]) {
237 // we're deleting an add. If there's a ckme, keep as a delete
238 if(!ckme) {
239 // Otherwise, remove from outgoingqueue and don't make a new OQE.
240 [existingOQE deleteFromDatabase:error];
241 return nil;
242 }
243 }
244 }
245
246 if([existingOQE.action isEqual: SecCKKSActionDelete] && [action isEqual:SecCKKSActionAdd]) {
247 actualAction = SecCKKSActionModify;
248 }
249 } else if(fetchError) {
250 ckkserror("ckksitem", zoneID, "Unable to fetch an existing OQE due to error: %@", fetchError);
251 fetchError = nil;
252
253 } else {
254 if(!ckme && [action isEqualToString:SecCKKSActionDelete]) {
255 CKKSOutgoingQueueEntry* anyExistingOQE = [CKKSOutgoingQueueEntry tryFromDatabase:uuid zoneID:zoneID error:&fetchError];
256
257 if(fetchError) {
258 ckkserror("ckksitem", zoneID, "Unable to fetch an existing OQE (any state) due to error: %@", fetchError);
259 } else if(!anyExistingOQE) {
260 // This is a delete for an item which doesn't exist. Therefore, this is a no-op.
261 ckkserror("ckksitem", zoneID, "Asked to delete a record for which we don't have a CKME or any OQE, ignoring: %@", uuid);
262 return nil;
263 }
264 }
265 }
266
267 newGenerationCount = ckme ? ckme.item.generationCount : (NSInteger) 0; // TODO: this is wrong
268
269 // Is this modification just changing the mdat? As a performance improvement, don't update the item in CK
270 if(ckme && !existingOQE && [actualAction isEqualToString:SecCKKSActionModify]) {
271 NSError* ckmeError = nil;
272 NSMutableDictionary* mirror = [[CKKSItemEncrypter decryptItemToDictionary:ckme.item error:&ckmeError] mutableCopy];
273 NSMutableDictionary* objdCopy = [objd mutableCopy];
274
275 if(ckmeError) {
276 ckkserror("ckksitem", zoneID, "Unable to decrypt current CKME: %@", ckmeError);
277 } else {
278 mirror[(__bridge id)kSecAttrModificationDate] = nil;
279 mirror[(__bridge id)kSecAttrSHA1] = nil;
280 objdCopy[(__bridge id)kSecAttrModificationDate] = nil;
281 objdCopy[(__bridge id)kSecAttrSHA1] = nil;
282
283 if([mirror isEqualToDictionary:objdCopy]) {
284 ckksnotice("ckksitem", zoneID, "Update to item only changes mdat; skipping %@", uuid);
285 return nil;
286 }
287 }
288 }
289
290 // Pull out any unencrypted fields
291 NSNumber* pcsServiceIdentifier = objd[(id)kSecAttrPCSPlaintextServiceIdentifier];
292 objd[(id)kSecAttrPCSPlaintextServiceIdentifier] = nil;
293
294 NSData* pcsPublicKey = objd[(id)kSecAttrPCSPlaintextPublicKey];
295 objd[(id)kSecAttrPCSPlaintextPublicKey] = nil;
296
297 NSData* pcsPublicIdentity = objd[(id)kSecAttrPCSPlaintextPublicIdentity];
298 objd[(id)kSecAttrPCSPlaintextPublicIdentity] = nil;
299
300 CKKSItem* baseitem = [[CKKSItem alloc] initWithUUID:uuid
301 parentKeyUUID:key.uuid
302 zoneID:zoneID
303 encodedCKRecord:nil
304 encItem:nil
305 wrappedkey:nil
306 generationCount:newGenerationCount
307 encver:currentCKKSItemEncryptionVersion
308 plaintextPCSServiceIdentifier:pcsServiceIdentifier
309 plaintextPCSPublicKey:pcsPublicKey
310 plaintextPCSPublicIdentity:pcsPublicIdentity];
311
312 if(!baseitem) {
313 NSError* localerror = [NSError errorWithDomain:CKKSErrorDomain code:CKKSItemCreationFailure description:@"Couldn't create an item" underlying:nil];
314 ckkserror("ckksitem", zoneID, "couldn't create an item: %@ %@", localerror, item);
315 if(error) {
316 *error = localerror;
317 }
318 return nil;
319 }
320
321 NSError* encryptionError = nil;
322 CKKSItem* encryptedItem = [CKKSItemEncrypter encryptCKKSItem:baseitem
323 dataDictionary:objd
324 updatingCKKSItem:ckme.item
325 parentkey:key
326 error:&encryptionError];
327
328 if(!encryptedItem || encryptionError) {
329 NSError* localerror = [NSError errorWithDomain:CKKSErrorDomain code:encryptionError.code description:@"Couldn't encrypt item" underlying:encryptionError];
330 ckkserror("ckksitem", zoneID, "couldn't encrypt item: %@ %@", localerror, item);
331 if(error) {
332 *error = localerror;
333 }
334 return nil;
335 }
336
337 return [[CKKSOutgoingQueueEntry alloc] initWithCKKSItem:encryptedItem
338 action:actualAction
339 state:SecCKKSStateNew
340 waitUntil:nil
341 accessGroup:accessgroup];
342 }
343
344 #pragma mark - Property access to underlying CKKSItem
345
346 -(NSString*)uuid {
347 return self.item.uuid;
348 }
349
350 -(void)setUuid:(NSString *)uuid {
351 self.item.uuid = uuid;
352 }
353
354 #pragma mark - Database Operations
355
356 + (instancetype) fromDatabase: (NSString*) uuid state: (NSString*) state zoneID:(CKRecordZoneID*)zoneID error: (NSError * __autoreleasing *) error {
357 return [self fromDatabaseWhere: @{@"UUID": CKKSNilToNSNull(uuid), @"state": CKKSNilToNSNull(state), @"ckzone":CKKSNilToNSNull(zoneID.zoneName)} error: error];
358 }
359
360 + (instancetype) tryFromDatabase: (NSString*) uuid zoneID:(CKRecordZoneID*)zoneID error: (NSError * __autoreleasing *) error {
361 return [self tryFromDatabaseWhere: @{@"UUID": CKKSNilToNSNull(uuid), @"ckzone":CKKSNilToNSNull(zoneID.zoneName)} error: error];
362 }
363
364 + (instancetype) tryFromDatabase: (NSString*) uuid state: (NSString*) state zoneID:(CKRecordZoneID*)zoneID error: (NSError * __autoreleasing *) error {
365 return [self tryFromDatabaseWhere: @{@"UUID": CKKSNilToNSNull(uuid), @"state":CKKSNilToNSNull(state), @"ckzone":CKKSNilToNSNull(zoneID.zoneName)} error: error];
366 }
367
368 + (NSArray<CKKSOutgoingQueueEntry*>*) fetch:(ssize_t) n state: (NSString*) state zoneID:(CKRecordZoneID*)zoneID error: (NSError * __autoreleasing *) error {
369 return [self fetch:n where: @{@"state":CKKSNilToNSNull(state), @"ckzone":CKKSNilToNSNull(zoneID.zoneName)} error:error];
370 }
371
372 + (NSArray<CKKSOutgoingQueueEntry*>*) allInState: (NSString*) state zoneID:(CKRecordZoneID*)zoneID error: (NSError * __autoreleasing *) error {
373 return [self allWhere: @{@"state":CKKSNilToNSNull(state), @"ckzone":CKKSNilToNSNull(zoneID.zoneName)} error:error];
374 }
375
376 + (NSArray<CKKSOutgoingQueueEntry*>*)allWithUUID:(NSString*)uuid states:(NSArray<NSString*>*)states zoneID:(CKRecordZoneID*)zoneID error:(NSError * __autoreleasing *)error
377 {
378 return [self allWhere:@{@"UUID": CKKSNilToNSNull(uuid),
379 @"state": [[CKKSSQLWhereIn alloc] initWithValues:states],
380 @"ckzone":CKKSNilToNSNull(zoneID.zoneName)}
381 error:error];
382 }
383
384
385 #pragma mark - CKKSSQLDatabaseObject methods
386
387 + (NSString*)sqlTable {
388 return @"outgoingqueue";
389 }
390
391 + (NSArray<NSString*>*)sqlColumns {
392 return [[CKKSItem sqlColumns] arrayByAddingObjectsFromArray: @[@"action", @"state", @"waituntil", @"accessgroup"]];
393 }
394
395 - (NSDictionary<NSString*,NSString*>*)whereClauseToFindSelf {
396 return @{@"UUID": self.uuid, @"state": self.state, @"ckzone":self.item.zoneID.zoneName};
397 }
398
399 - (NSDictionary<NSString*,NSString*>*)sqlValues {
400 NSISO8601DateFormatter* dateFormat = [[NSISO8601DateFormatter alloc] init];
401
402 NSMutableDictionary* values = [[self.item sqlValues] mutableCopy];
403 values[@"action"] = self.action;
404 values[@"state"] = self.state;
405 values[@"waituntil"] = CKKSNilToNSNull(self.waitUntil ? [dateFormat stringFromDate: self.waitUntil] : nil);
406 values[@"accessgroup"] = self.accessgroup;
407
408 return values;
409 }
410
411
412 + (instancetype)fromDatabaseRow:(NSDictionary<NSString *, CKKSSQLResult*>*) row {
413 return [[CKKSOutgoingQueueEntry alloc] initWithCKKSItem:[CKKSItem fromDatabaseRow: row]
414 action:row[@"action"].asString
415 state:row[@"state"].asString
416 waitUntil:row[@"waituntil"].asISO8601Date
417 accessGroup:row[@"accessgroup"].asString];
418 }
419
420 + (NSDictionary<NSString*,NSNumber*>*)countsByStateInZone:(CKRecordZoneID*)zoneID error: (NSError * __autoreleasing *) error {
421 NSMutableDictionary* results = [[NSMutableDictionary alloc] init];
422
423 [CKKSSQLDatabaseObject queryDatabaseTable: [[self class] sqlTable]
424 where: @{@"ckzone": CKKSNilToNSNull(zoneID.zoneName)}
425 columns: @[@"state", @"count(rowid)"]
426 groupBy: @[@"state"]
427 orderBy:nil
428 limit: -1
429 processRow: ^(NSDictionary<NSString*, CKKSSQLResult*>* row) {
430 results[row[@"state"].asString] = row[@"count(rowid)"].asNSNumberInteger;
431 }
432 error: error];
433 return results;
434 }
435
436 + (NSInteger)countByState:(CKKSItemState *)state zone:(CKRecordZoneID*)zoneID error: (NSError * __autoreleasing *) error {
437 __block NSInteger result = -1;
438
439 [CKKSSQLDatabaseObject queryDatabaseTable: [[self class] sqlTable]
440 where: @{@"ckzone": CKKSNilToNSNull(zoneID.zoneName), @"state": state }
441 columns: @[@"count(*)"]
442 groupBy: nil
443 orderBy: nil
444 limit: -1
445 processRow: ^(NSDictionary<NSString*, CKKSSQLResult*>* row) {
446 result = row[@"count(*)"].asNSInteger;
447 }
448 error: error];
449 return result;
450 }
451
452
453 @end
454
455 #endif