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