]> git.saurik.com Git - apple/security.git/blob - keychain/ckks/CKKSOutgoingQueueEntry.m
Security-59306.11.20.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 <securityd/SecDbItem.h>
34 #include <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 + (instancetype)withItem:(SecDbItemRef)item action:(NSString*)action ckks:(CKKSKeychainView*)ckks error: (NSError * __autoreleasing *) error {
90 CFErrorRef cferror = NULL;
91 CKKSKey* key = nil;
92 NSString* uuid = nil;
93 NSString* accessgroup = nil;
94
95 NSInteger newGenerationCount = -1;
96
97 NSMutableDictionary* objd = nil;
98
99 NSError* keyError = nil;
100 key = [ckks keyForItem:item error:&keyError];
101 if(!key || keyError) {
102 NSError* localerror = [NSError errorWithDomain:CKKSErrorDomain code:keyError.code description:@"No key for item" underlying:keyError];
103 ckkserror("ckksitem", ckks, "no key for item: %@ %@", localerror, item);
104 if(error) {
105 *error = localerror;
106 }
107 return nil;
108 }
109
110 objd = (__bridge_transfer NSMutableDictionary*) SecDbItemCopyPListWithMask(item, kSecDbSyncFlag, &cferror);
111 if(!objd) {
112 NSError* localerror = [NSError errorWithDomain:CKKSErrorDomain code:CFErrorGetCode(cferror) description:@"Couldn't create object plist" underlying:(__bridge_transfer NSError*)cferror];
113 ckkserror("ckksitem", ckks, "no plist: %@ %@", localerror, item);
114 if(error) {
115 *error = localerror;
116 }
117 return nil;
118 }
119
120 // Object classes aren't in the item plist, set them specifically
121 [objd setObject: (__bridge NSString*) item->class->name forKey: (__bridge NSString*) kSecClass];
122
123 uuid = (__bridge_transfer NSString*) CFRetainSafe(SecDbItemGetValue(item, &v10itemuuid, &cferror));
124 if(!uuid || cferror) {
125 NSError* localerror = [NSError errorWithDomain:CKKSErrorDomain code:CKKSNoUUIDOnItem description:@"No UUID for item" underlying:(__bridge_transfer NSError*)cferror];
126 ckkserror("ckksitem", ckks, "No UUID for item: %@ %@", localerror, item);
127 if(error) {
128 *error = localerror;
129 }
130 return nil;
131 }
132 if([uuid isKindOfClass:[NSNull class]]) {
133 NSError* localerror = [NSError errorWithDomain:CKKSErrorDomain code:CKKSNoUUIDOnItem description:@"UUID not found in object" underlying:nil];
134 ckkserror("ckksitem", ckks, "couldn't fetch UUID: %@ %@", localerror, item);
135 if(error) {
136 *error = localerror;
137 }
138 return nil;
139 }
140
141 accessgroup = (__bridge_transfer NSString*) CFRetainSafe(SecDbItemGetValue(item, &v6agrp, &cferror));
142 if(!accessgroup || cferror) {
143 NSError* localerror = [NSError errorWithDomain:CKKSErrorDomain code:CFErrorGetCode(cferror) description:@"accessgroup not found in object" underlying:(__bridge_transfer NSError*)cferror];
144 ckkserror("ckksitem", ckks, "couldn't fetch access group from item: %@ %@", localerror, item);
145 if(error) {
146 *error = localerror;
147 }
148 return nil;
149 }
150 if([accessgroup isKindOfClass:[NSNull class]]) {
151 // That's okay; this is only used for rate limiting.
152 ckkserror("ckksitem", ckks, "couldn't fetch accessgroup: %@", item);
153 accessgroup = @"no-group";
154 }
155
156 CKKSMirrorEntry* ckme = [CKKSMirrorEntry tryFromDatabase:uuid zoneID:ckks.zoneID error:error];
157
158 // The action this change should be depends on any existing pending action, if any
159 // Particularly, we need to coalesce (existing action, new action) to:
160 // (add, modify) => add
161 // (add, delete) => no-op
162 // (delete, add) => modify
163 NSString* actualAction = action;
164
165 CKKSOutgoingQueueEntry* existingOQE = [CKKSOutgoingQueueEntry tryFromDatabase:uuid state:SecCKKSStateNew zoneID:ckks.zoneID error:error];
166 if(existingOQE) {
167 if([existingOQE.action isEqual: SecCKKSActionAdd]) {
168 if([action isEqual:SecCKKSActionModify]) {
169 actualAction = SecCKKSActionAdd;
170 } else if([action isEqual:SecCKKSActionDelete]) {
171 // we're deleting an add. If there's a ckme, keep as a delete
172 if(!ckme) {
173 // Otherwise, remove from outgoingqueue and don't make a new OQE.
174 [existingOQE deleteFromDatabase:error];
175 return nil;
176 }
177 }
178 }
179
180 if([existingOQE.action isEqual: SecCKKSActionDelete] && [action isEqual:SecCKKSActionAdd]) {
181 actualAction = SecCKKSActionModify;
182 }
183 }
184
185 newGenerationCount = ckme ? ckme.item.generationCount : (NSInteger) 0; // TODO: this is wrong
186
187 // Pull out any unencrypted fields
188 NSNumber* pcsServiceIdentifier = objd[(id)kSecAttrPCSPlaintextServiceIdentifier];
189 objd[(id)kSecAttrPCSPlaintextServiceIdentifier] = nil;
190
191 NSData* pcsPublicKey = objd[(id)kSecAttrPCSPlaintextPublicKey];
192 objd[(id)kSecAttrPCSPlaintextPublicKey] = nil;
193
194 NSData* pcsPublicIdentity = objd[(id)kSecAttrPCSPlaintextPublicIdentity];
195 objd[(id)kSecAttrPCSPlaintextPublicIdentity] = nil;
196
197 CKKSItem* baseitem = [[CKKSItem alloc] initWithUUID:uuid
198 parentKeyUUID:key.uuid
199 zoneID:ckks.zoneID
200 encodedCKRecord:nil
201 encItem:nil
202 wrappedkey:nil
203 generationCount:newGenerationCount
204 encver:currentCKKSItemEncryptionVersion
205 plaintextPCSServiceIdentifier:pcsServiceIdentifier
206 plaintextPCSPublicKey:pcsPublicKey
207 plaintextPCSPublicIdentity:pcsPublicIdentity];
208
209 if(!baseitem) {
210 NSError* localerror = [NSError errorWithDomain:CKKSErrorDomain code:CKKSItemCreationFailure description:@"Couldn't create an item" underlying:nil];
211 ckkserror("ckksitem", ckks, "couldn't create an item: %@ %@", localerror, item);
212 if(error) {
213 *error = localerror;
214 }
215 return nil;
216 }
217
218 NSError* encryptionError = nil;
219 CKKSItem* encryptedItem = [CKKSItemEncrypter encryptCKKSItem:baseitem
220 dataDictionary:objd
221 updatingCKKSItem:ckme.item
222 parentkey:key
223 error:&encryptionError];
224
225 if(!encryptedItem || encryptionError) {
226 NSError* localerror = [NSError errorWithDomain:CKKSErrorDomain code:encryptionError.code description:@"Couldn't encrypt item" underlying:encryptionError];
227 ckkserror("ckksitem", ckks, "couldn't encrypt item: %@ %@", localerror, item);
228 if(error) {
229 *error = localerror;
230 }
231 return nil;
232 }
233
234 return [[CKKSOutgoingQueueEntry alloc] initWithCKKSItem:encryptedItem
235 action:actualAction
236 state:SecCKKSStateNew
237 waitUntil:nil
238 accessGroup:accessgroup];
239 }
240
241 #pragma mark - Property access to underlying CKKSItem
242
243 -(NSString*)uuid {
244 return self.item.uuid;
245 }
246
247 -(void)setUuid:(NSString *)uuid {
248 self.item.uuid = uuid;
249 }
250
251 #pragma mark - Database Operations
252
253 + (instancetype) fromDatabase: (NSString*) uuid state: (NSString*) state zoneID:(CKRecordZoneID*)zoneID error: (NSError * __autoreleasing *) error {
254 return [self fromDatabaseWhere: @{@"UUID": CKKSNilToNSNull(uuid), @"state": CKKSNilToNSNull(state), @"ckzone":CKKSNilToNSNull(zoneID.zoneName)} error: error];
255 }
256
257 + (instancetype) tryFromDatabase: (NSString*) uuid zoneID:(CKRecordZoneID*)zoneID error: (NSError * __autoreleasing *) error {
258 return [self tryFromDatabaseWhere: @{@"UUID": CKKSNilToNSNull(uuid), @"ckzone":CKKSNilToNSNull(zoneID.zoneName)} error: error];
259 }
260
261 + (instancetype) tryFromDatabase: (NSString*) uuid state: (NSString*) state zoneID:(CKRecordZoneID*)zoneID error: (NSError * __autoreleasing *) error {
262 return [self tryFromDatabaseWhere: @{@"UUID": CKKSNilToNSNull(uuid), @"state":CKKSNilToNSNull(state), @"ckzone":CKKSNilToNSNull(zoneID.zoneName)} error: error];
263 }
264
265 + (NSArray<CKKSOutgoingQueueEntry*>*) fetch:(ssize_t) n state: (NSString*) state zoneID:(CKRecordZoneID*)zoneID error: (NSError * __autoreleasing *) error {
266 return [self fetch:n where: @{@"state":CKKSNilToNSNull(state), @"ckzone":CKKSNilToNSNull(zoneID.zoneName)} error:error];
267 }
268
269 + (NSArray<CKKSOutgoingQueueEntry*>*) allInState: (NSString*) state zoneID:(CKRecordZoneID*)zoneID error: (NSError * __autoreleasing *) error {
270 return [self allWhere: @{@"state":CKKSNilToNSNull(state), @"ckzone":CKKSNilToNSNull(zoneID.zoneName)} error:error];
271 }
272
273
274 #pragma mark - CKKSSQLDatabaseObject methods
275
276 + (NSString*)sqlTable {
277 return @"outgoingqueue";
278 }
279
280 + (NSArray<NSString*>*)sqlColumns {
281 return [[CKKSItem sqlColumns] arrayByAddingObjectsFromArray: @[@"action", @"state", @"waituntil", @"accessgroup"]];
282 }
283
284 - (NSDictionary<NSString*,NSString*>*)whereClauseToFindSelf {
285 return @{@"UUID": self.uuid, @"state": self.state, @"ckzone":self.item.zoneID.zoneName};
286 }
287
288 - (NSDictionary<NSString*,NSString*>*)sqlValues {
289 NSISO8601DateFormatter* dateFormat = [[NSISO8601DateFormatter alloc] init];
290
291 NSMutableDictionary* values = [[self.item sqlValues] mutableCopy];
292 values[@"action"] = self.action;
293 values[@"state"] = self.state;
294 values[@"waituntil"] = CKKSNilToNSNull(self.waitUntil ? [dateFormat stringFromDate: self.waitUntil] : nil);
295 values[@"accessgroup"] = self.accessgroup;
296
297 return values;
298 }
299
300
301 + (instancetype)fromDatabaseRow:(NSDictionary<NSString *, CKKSSQLResult*>*) row {
302 return [[CKKSOutgoingQueueEntry alloc] initWithCKKSItem:[CKKSItem fromDatabaseRow: row]
303 action:row[@"action"].asString
304 state:row[@"state"].asString
305 waitUntil:row[@"waituntil"].asISO8601Date
306 accessGroup:row[@"accessgroup"].asString];
307 }
308
309 + (NSDictionary<NSString*,NSNumber*>*)countsByStateInZone:(CKRecordZoneID*)zoneID error: (NSError * __autoreleasing *) error {
310 NSMutableDictionary* results = [[NSMutableDictionary alloc] init];
311
312 [CKKSSQLDatabaseObject queryDatabaseTable: [[self class] sqlTable]
313 where: @{@"ckzone": CKKSNilToNSNull(zoneID.zoneName)}
314 columns: @[@"state", @"count(rowid)"]
315 groupBy: @[@"state"]
316 orderBy:nil
317 limit: -1
318 processRow: ^(NSDictionary<NSString*, CKKSSQLResult*>* row) {
319 results[row[@"state"].asString] = row[@"count(rowid)"].asNSNumberInteger;
320 }
321 error: error];
322 return results;
323 }
324
325 + (NSInteger)countByState:(CKKSItemState *)state zone:(CKRecordZoneID*)zoneID error: (NSError * __autoreleasing *) error {
326 __block NSInteger result = -1;
327
328 [CKKSSQLDatabaseObject queryDatabaseTable: [[self class] sqlTable]
329 where: @{@"ckzone": CKKSNilToNSNull(zoneID.zoneName), @"state": state }
330 columns: @[@"count(*)"]
331 groupBy: nil
332 orderBy: nil
333 limit: -1
334 processRow: ^(NSDictionary<NSString*, CKKSSQLResult*>* row) {
335 result = row[@"count(*)"].asNSInteger;
336 }
337 error: error];
338 return result;
339 }
340
341
342 @end
343
344 #endif