]>
Commit | Line | Data |
---|---|---|
866f8763 A |
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 "CKKSViewManager.h" | |
25 | #import "CKKSKeychainView.h" | |
26 | #import "CKKSCurrentKeyPointer.h" | |
27 | #import "CKKSKey.h" | |
28 | #include <securityd/SecItemSchema.h> | |
29 | #include <Security/SecItem.h> | |
30 | #include <Security/SecItemPriv.h> | |
31 | ||
32 | #if OCTAGON | |
33 | ||
34 | #include <CloudKit/CloudKit.h> | |
35 | #include <CloudKit/CloudKit_Private.h> | |
36 | ||
37 | @implementation CKKSKey | |
38 | ||
39 | - (instancetype)init { | |
40 | self = [super init]; | |
41 | return self; | |
42 | } | |
43 | ||
44 | - (instancetype) initSelfWrappedWithAESKey: (CKKSAESSIVKey*) aeskey | |
45 | uuid: (NSString*) uuid | |
46 | keyclass: (CKKSKeyClass*)keyclass | |
47 | state: (CKKSProcessedState*) state | |
48 | zoneID: (CKRecordZoneID*) zoneID | |
49 | encodedCKRecord: (NSData*) encodedrecord | |
50 | currentkey: (NSInteger) currentkey | |
51 | { | |
52 | if(self = [super initWithUUID: uuid | |
53 | parentKeyUUID: uuid | |
54 | zoneID: zoneID | |
55 | encodedCKRecord: encodedrecord | |
56 | encItem: nil | |
57 | wrappedkey: nil | |
58 | generationCount: 0 | |
59 | encver: currentCKKSItemEncryptionVersion]) { | |
60 | _keyclass = keyclass; | |
61 | _currentkey = !!currentkey; | |
62 | _aessivkey = aeskey; | |
63 | _state = state; | |
64 | ||
65 | self.ckRecordType = SecCKRecordIntermediateKeyType; | |
66 | ||
67 | // Wrap the key with the key. Not particularly useful, but there you go. | |
68 | NSError* error = nil; | |
69 | [self wrapUnder: self error:&error]; | |
70 | if(error != nil) { | |
71 | secerror("CKKSKey: Couldn't self-wrap key: %@", error); | |
72 | return nil; | |
73 | } | |
74 | } | |
75 | return self; | |
76 | } | |
77 | ||
78 | - (instancetype) initWrappedBy: (CKKSKey*) wrappingKey | |
79 | AESKey: (CKKSAESSIVKey*) aeskey | |
80 | uuid: (NSString*) uuid | |
81 | keyclass: (CKKSKeyClass*)keyclass | |
82 | state: (CKKSProcessedState*) state | |
83 | zoneID: (CKRecordZoneID*) zoneID | |
84 | encodedCKRecord: (NSData*) encodedrecord | |
85 | currentkey: (NSInteger) currentkey | |
86 | { | |
87 | if(self = [super initWithUUID: uuid | |
88 | parentKeyUUID: wrappingKey.uuid | |
89 | zoneID: zoneID | |
90 | encodedCKRecord: encodedrecord | |
91 | encItem:nil | |
92 | wrappedkey:nil | |
93 | generationCount:0 | |
94 | encver: | |
95 | currentCKKSItemEncryptionVersion]) { | |
96 | _keyclass = keyclass; | |
97 | _currentkey = !!currentkey; | |
98 | _aessivkey = aeskey; | |
99 | _state = state; | |
100 | ||
101 | self.ckRecordType = SecCKRecordIntermediateKeyType; | |
102 | ||
103 | NSError* error = nil; | |
104 | [self wrapUnder: wrappingKey error:&error]; | |
105 | if(error != nil) { | |
106 | secerror("CKKSKey: Couldn't wrap key with key: %@", error); | |
107 | return nil; | |
108 | } | |
109 | } | |
110 | return self; | |
111 | } | |
112 | ||
113 | - (instancetype) initWithWrappedAESKey: (CKKSWrappedAESSIVKey*) wrappedaeskey | |
114 | uuid: (NSString*) uuid | |
115 | parentKeyUUID: (NSString*) parentKeyUUID | |
116 | keyclass: (CKKSKeyClass*)keyclass | |
117 | state: (CKKSProcessedState*) state | |
118 | zoneID: (CKRecordZoneID*) zoneID | |
119 | encodedCKRecord: (NSData*) encodedrecord | |
120 | currentkey: (NSInteger) currentkey | |
121 | { | |
122 | if(self = [super initWithUUID:uuid | |
123 | parentKeyUUID:parentKeyUUID | |
124 | zoneID:zoneID | |
125 | encodedCKRecord:encodedrecord | |
126 | encItem:nil | |
127 | wrappedkey:wrappedaeskey | |
128 | generationCount:0 | |
129 | encver:currentCKKSItemEncryptionVersion]) { | |
130 | _keyclass = keyclass; | |
131 | _currentkey = !!currentkey; | |
132 | _aessivkey = nil; | |
133 | _state = state; | |
134 | ||
135 | self.ckRecordType = SecCKRecordIntermediateKeyType; | |
136 | } | |
137 | return self; | |
138 | } | |
139 | ||
140 | - (void)dealloc { | |
141 | [self zeroKeys]; | |
142 | } | |
143 | ||
144 | - (void)zeroKeys { | |
145 | [self.aessivkey zeroKey]; | |
146 | } | |
147 | ||
148 | - (bool)wrapsSelf { | |
149 | return [self.uuid isEqual: self.parentKeyUUID]; | |
150 | } | |
151 | ||
152 | - (bool)wrapUnder: (CKKSKey*) wrappingKey error: (NSError * __autoreleasing *) error { | |
153 | self.wrappedkey = [wrappingKey wrapAESKey: self.aessivkey error:error]; | |
154 | if(self.wrappedkey == nil) { | |
155 | secerror("CKKSKey: couldn't wrap key: %@", error ? *error : @"unknown error"); | |
156 | } else { | |
157 | self.parentKeyUUID = wrappingKey.uuid; | |
158 | } | |
159 | return (self.wrappedkey != nil); | |
160 | } | |
161 | ||
162 | - (bool)unwrapSelfWithAESKey: (CKKSAESSIVKey*) unwrappingKey error: (NSError * __autoreleasing *) error { | |
163 | _aessivkey = [unwrappingKey unwrapAESKey:self.wrappedkey error:error]; | |
164 | return (_aessivkey != nil); | |
165 | } | |
166 | ||
167 | + (instancetype) loadKeyWithUUID: (NSString*) uuid zoneID:(CKRecordZoneID*)zoneID error: (NSError * __autoreleasing *) error { | |
168 | CKKSKey* key = [CKKSKey fromDatabase: uuid zoneID:zoneID error:error]; | |
169 | ||
170 | // failed unwrapping means we can't return a key. | |
171 | if(![key ensureKeyLoaded:error]) { | |
172 | return nil; | |
173 | } | |
174 | return key; | |
175 | } | |
176 | ||
177 | + (CKKSKey*) randomKeyWrappedByParent: (CKKSKey*) parentKey error: (NSError * __autoreleasing *) error { | |
178 | return [self randomKeyWrappedByParent: parentKey keyclass:parentKey.keyclass error:error]; | |
179 | } | |
180 | ||
181 | + (CKKSKey*) randomKeyWrappedByParent: (CKKSKey*) parentKey keyclass:(CKKSKeyClass*)keyclass error: (NSError * __autoreleasing *) error { | |
182 | CKKSAESSIVKey* aessivkey = [CKKSAESSIVKey randomKey]; | |
183 | ||
184 | CKKSKey* key = [[CKKSKey alloc] initWrappedBy: parentKey | |
185 | AESKey: aessivkey | |
186 | uuid:[[NSUUID UUID] UUIDString] | |
187 | keyclass:keyclass | |
188 | state:SecCKKSProcessedStateLocal | |
189 | zoneID: parentKey.zoneID | |
190 | encodedCKRecord: nil | |
191 | currentkey: false]; | |
192 | return key; | |
193 | } | |
194 | ||
195 | + (instancetype)randomKeyWrappedBySelf: (CKRecordZoneID*) zoneID error: (NSError * __autoreleasing *) error { | |
196 | CKKSAESSIVKey* aessivkey = [CKKSAESSIVKey randomKey]; | |
197 | NSString* uuid = [[NSUUID UUID] UUIDString]; | |
198 | ||
199 | CKKSKey* key = [[CKKSKey alloc] initSelfWrappedWithAESKey: aessivkey | |
200 | uuid:uuid | |
201 | keyclass:SecCKKSKeyClassTLK | |
202 | state:SecCKKSProcessedStateLocal | |
203 | zoneID: zoneID | |
204 | encodedCKRecord: nil | |
205 | currentkey: false]; | |
206 | return key; | |
207 | ||
208 | } | |
209 | ||
210 | - (CKKSKey*)topKeyInAnyState: (NSError * __autoreleasing *) error { | |
211 | // Recursively find the top-level key in the hierarchy, preferring 'remote' keys. | |
212 | if([self wrapsSelf]) { | |
213 | return self; | |
214 | } | |
215 | ||
216 | CKKSKey* remoteParent = [CKKSKey tryFromDatabaseWhere: @{@"UUID": self.parentKeyUUID, @"state": SecCKKSProcessedStateRemote} error: error]; | |
217 | if(remoteParent) { | |
218 | return [remoteParent topKeyInAnyState: error]; | |
219 | } | |
220 | ||
221 | // No remote parent. Fall back to anything. | |
222 | CKKSKey* parent = [CKKSKey fromDatabaseWhere: @{@"UUID": self.parentKeyUUID} error: error]; | |
223 | if(parent) { | |
224 | return [parent topKeyInAnyState: error]; | |
225 | } | |
226 | ||
227 | // No good. Error is already filled. | |
228 | return nil; | |
229 | } | |
230 | ||
231 | - (CKKSAESSIVKey*)ensureKeyLoaded: (NSError * __autoreleasing *) error { | |
232 | if(self.aessivkey) { | |
233 | return self.aessivkey; | |
234 | } | |
235 | ||
236 | // Attempt to load this key from the keychain | |
237 | if([self loadKeyMaterialFromKeychain:error]) { | |
238 | return self.aessivkey; | |
239 | } | |
240 | ||
241 | return nil; | |
242 | } | |
243 | ||
244 | ||
245 | - (CKKSAESSIVKey*)unwrapViaKeyHierarchy: (NSError * __autoreleasing *) error { | |
246 | if(self.aessivkey) { | |
247 | return self.aessivkey; | |
248 | } | |
249 | ||
250 | NSError* localerror = nil; | |
251 | ||
252 | // Attempt to load this key from the keychain | |
253 | if([self loadKeyMaterialFromKeychain:&localerror]) { | |
254 | // Rad. Success! | |
255 | return self.aessivkey; | |
256 | } | |
257 | ||
258 | // First, check if we're a TLK. | |
259 | if([self.keyclass isEqual: SecCKKSKeyClassTLK]) { | |
260 | // Okay, not loading the key from the keychain above is an issue. If we have a parent key, then fall through to the recursion below. | |
261 | if(!self.parentKeyUUID || [self.parentKeyUUID isEqualToString: self.uuid]) { | |
262 | if(error) { | |
263 | *error = localerror; | |
264 | } | |
265 | return nil; | |
266 | } | |
267 | } | |
268 | ||
269 | // Recursively unwrap our parent. | |
270 | CKKSKey* parent = [CKKSKey fromDatabaseAnyState:self.parentKeyUUID zoneID:self.zoneID error:error]; | |
271 | ||
272 | // TODO: do we need loop detection here? | |
273 | if(![parent unwrapViaKeyHierarchy: error]) { | |
274 | return nil; | |
275 | } | |
276 | ||
277 | _aessivkey = [parent unwrapAESKey:self.wrappedkey error:error]; | |
278 | return self.aessivkey; | |
279 | } | |
280 | ||
281 | - (CKKSWrappedAESSIVKey*)wrapAESKey: (CKKSAESSIVKey*) keyToWrap error: (NSError * __autoreleasing *) error { | |
282 | CKKSAESSIVKey* key = [self ensureKeyLoaded: error]; | |
283 | CKKSWrappedAESSIVKey* wrappedkey = [key wrapAESKey: keyToWrap error:error]; | |
284 | return wrappedkey; | |
285 | } | |
286 | ||
287 | - (CKKSAESSIVKey*)unwrapAESKey: (CKKSWrappedAESSIVKey*) keyToUnwrap error: (NSError * __autoreleasing *) error { | |
288 | CKKSAESSIVKey* key = [self ensureKeyLoaded: error]; | |
289 | CKKSAESSIVKey* unwrappedkey = [key unwrapAESKey: keyToUnwrap error:error]; | |
290 | return unwrappedkey; | |
291 | } | |
292 | ||
293 | - (NSData*)encryptData: (NSData*) plaintext authenticatedData: (NSDictionary<NSString*, NSData*>*) ad error: (NSError * __autoreleasing *) error { | |
294 | CKKSAESSIVKey* key = [self ensureKeyLoaded: error]; | |
295 | NSData* data = [key encryptData: plaintext authenticatedData:ad error:error]; | |
296 | return data; | |
297 | } | |
298 | ||
299 | - (NSData*)decryptData: (NSData*) ciphertext authenticatedData: (NSDictionary<NSString*, NSData*>*) ad error: (NSError * __autoreleasing *) error { | |
300 | CKKSAESSIVKey* key = [self ensureKeyLoaded: error]; | |
301 | NSData* data = [key decryptData: ciphertext authenticatedData:ad error:error]; | |
302 | return data; | |
303 | } | |
304 | ||
305 | /* Functions to load and save keys from the keychain (where we get to store actual key material!) */ | |
306 | - (bool)saveKeyMaterialToKeychain: (NSError * __autoreleasing *) error { | |
307 | return [self saveKeyMaterialToKeychain: true error: error]; | |
308 | } | |
309 | ||
310 | - (bool)saveKeyMaterialToKeychain: (bool)stashTLK error:(NSError * __autoreleasing *) error { | |
311 | return [CKKSKey saveKeyMaterialToKeychain:self stashTLK:stashTLK error:error]; | |
312 | } | |
313 | ||
314 | +(bool)saveKeyMaterialToKeychain:(CKKSKey*)key stashTLK:(bool)stashTLK error:(NSError * __autoreleasing *) error { | |
315 | ||
316 | // Note that we only store the key class, view, UUID, parentKeyUUID, and key material in the keychain | |
317 | // Any other metadata must be stored elsewhere and filled in at load time. | |
318 | ||
319 | if(![key ensureKeyLoaded:error]) { | |
320 | // No key material, nothing to save to keychain. | |
321 | return false; | |
322 | } | |
323 | ||
324 | // iOS keychains can't store symmetric keys, so we're reduced to storing this key as a password | |
325 | NSData* keydata = [[[NSData alloc] initWithBytes:key.aessivkey->key length:key.aessivkey->size] base64EncodedDataWithOptions:0]; | |
326 | NSMutableDictionary* query = [@{ | |
327 | (id)kSecClass : (id)kSecClassInternetPassword, | |
328 | (id)kSecAttrAccessible: (id)kSecAttrAccessibleWhenUnlocked, | |
329 | (id)kSecAttrNoLegacy : @YES, | |
330 | (id)kSecAttrAccessGroup: @"com.apple.security.ckks", | |
331 | (id)kSecAttrDescription: key.keyclass, | |
332 | (id)kSecAttrServer: key.zoneID.zoneName, | |
333 | (id)kSecAttrAccount: key.uuid, | |
334 | (id)kSecAttrPath: key.parentKeyUUID, | |
335 | (id)kSecAttrIsInvisible: @YES, | |
336 | (id)kSecValueData : keydata, | |
337 | } mutableCopy]; | |
338 | ||
339 | // Only TLKs are synchronizable. Other keyclasses must synchronize via key hierarchy. | |
340 | if([key.keyclass isEqualToString: SecCKKSKeyClassTLK]) { | |
341 | // Use PCS-MasterKey view so they'll be initial-synced under SOS. | |
342 | query[(id)kSecAttrSyncViewHint] = (id)kSecAttrViewHintPCSMasterKey; | |
343 | query[(id)kSecAttrSynchronizable] = (id)kCFBooleanTrue; | |
344 | } | |
345 | ||
346 | // Class C keys are accessible after first unlock; TLKs and Class A keys are accessible only when unlocked | |
347 | if([key.keyclass isEqualToString: SecCKKSKeyClassC]) { | |
348 | query[(id)kSecAttrAccessible] = (id)kSecAttrAccessibleAfterFirstUnlock; | |
349 | } else { | |
350 | query[(id)kSecAttrAccessible] = (id)kSecAttrAccessibleWhenUnlocked; | |
351 | } | |
352 | ||
353 | OSStatus status = SecItemAdd((__bridge CFDictionaryRef) query, NULL); | |
354 | ||
355 | if(status == errSecDuplicateItem) { | |
356 | // Sure, okay, fine, we'll update. | |
357 | error = nil; | |
358 | NSMutableDictionary* update = [@{ | |
359 | (id)kSecValueData: keydata, | |
360 | (id)kSecAttrPath: key.parentKeyUUID, | |
361 | } mutableCopy]; | |
362 | query[(id)kSecValueData] = nil; | |
363 | query[(id)kSecAttrPath] = nil; | |
364 | ||
365 | // Udpate the view-hint, too | |
366 | if([key.keyclass isEqualToString: SecCKKSKeyClassTLK]) { | |
367 | update[(id)kSecAttrSyncViewHint] = (id)kSecAttrViewHintPCSMasterKey; | |
368 | query[(id)kSecAttrSyncViewHint] = nil; | |
369 | } | |
370 | ||
371 | status = SecItemUpdate((__bridge CFDictionaryRef) query, (__bridge CFDictionaryRef)update); | |
372 | } | |
373 | ||
374 | if(status && error) { | |
375 | *error = [NSError errorWithDomain:@"securityd" | |
376 | code:status | |
377 | userInfo:@{NSLocalizedDescriptionKey: | |
378 | [NSString stringWithFormat:@"Couldn't save %@ to keychain: %d", self, (int)status]}]; | |
379 | } | |
380 | ||
381 | // TLKs are synchronizable. Stash them nonsyncably nearby. | |
382 | // Don't report errors here. | |
383 | if(stashTLK && [key.keyclass isEqualToString: SecCKKSKeyClassTLK]) { | |
384 | query = [@{ | |
385 | (id)kSecClass : (id)kSecClassInternetPassword, | |
386 | (id)kSecAttrAccessible: (id)kSecAttrAccessibleWhenUnlocked, | |
387 | (id)kSecAttrNoLegacy : @YES, | |
388 | (id)kSecAttrAccessGroup: @"com.apple.security.ckks", | |
389 | (id)kSecAttrDescription: [key.keyclass stringByAppendingString: @"-nonsync"], | |
390 | (id)kSecAttrServer: key.zoneID.zoneName, | |
391 | (id)kSecAttrAccount: key.uuid, | |
392 | (id)kSecAttrPath: key.parentKeyUUID, | |
393 | (id)kSecAttrIsInvisible: @YES, | |
394 | (id)kSecValueData : keydata, | |
395 | } mutableCopy]; | |
396 | query[(id)kSecAttrSynchronizable] = (id)kCFBooleanFalse; | |
397 | ||
398 | OSStatus stashstatus = SecItemAdd((__bridge CFDictionaryRef) query, NULL); | |
399 | if(stashstatus != errSecSuccess) { | |
400 | if(stashstatus == errSecDuplicateItem) { | |
401 | // Sure, okay, fine, we'll update. | |
402 | error = nil; | |
403 | NSDictionary* update = @{ | |
404 | (id)kSecValueData: keydata, | |
405 | (id)kSecAttrPath: key.parentKeyUUID, | |
406 | }; | |
407 | query[(id)kSecValueData] = nil; | |
408 | query[(id)kSecAttrPath] = nil; | |
409 | ||
410 | stashstatus = SecItemUpdate((__bridge CFDictionaryRef) query, (__bridge CFDictionaryRef)update); | |
411 | } | |
412 | ||
413 | if(stashstatus != errSecSuccess) { | |
414 | secerror("ckkskey: Couldn't stash %@ to keychain: %d", self, (int)stashstatus); | |
415 | } | |
416 | } | |
417 | } | |
418 | ||
419 | return status == 0; | |
420 | } | |
421 | ||
422 | + (NSData*)loadKeyMaterialFromKeychain:(CKKSKey*)key resave:(bool*)resavePtr error:(NSError* __autoreleasing *)error { | |
423 | NSMutableDictionary* query = [@{ | |
424 | (id)kSecClass : (id)kSecClassInternetPassword, | |
425 | (id)kSecAttrNoLegacy : @YES, | |
426 | (id)kSecAttrAccessGroup : @"com.apple.security.ckks", | |
427 | (id)kSecAttrDescription: key.keyclass, | |
428 | (id)kSecAttrAccount: key.uuid, | |
429 | (id)kSecAttrServer: key.zoneID.zoneName, | |
430 | (id)kSecReturnAttributes: @YES, | |
431 | (id)kSecReturnData: @YES, | |
432 | } mutableCopy]; | |
433 | ||
434 | // Synchronizable items are only found if you request synchronizable items. Only TLKs are synchronizable. | |
435 | if([key.keyclass isEqualToString: SecCKKSKeyClassTLK]) { | |
436 | query[(id)kSecAttrSynchronizable] = (id)kCFBooleanTrue; | |
437 | } | |
438 | ||
439 | CFTypeRef result = NULL; | |
440 | OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef) query, &result); | |
441 | ||
442 | if(status == errSecItemNotFound) { | |
443 | CFReleaseNull(result); | |
444 | //didn't find a regular tlk? how about a piggy? | |
445 | if([key.keyclass isEqualToString: SecCKKSKeyClassTLK]) { | |
446 | query = [@{ | |
447 | (id)kSecClass : (id)kSecClassInternetPassword, | |
448 | (id)kSecAttrNoLegacy : @YES, | |
449 | (id)kSecAttrAccessGroup : @"com.apple.security.ckks", | |
450 | (id)kSecAttrDescription: @"tlk-piggy", | |
451 | (id)kSecAttrSynchronizable : (id)kSecAttrSynchronizableAny, | |
452 | (id)kSecAttrAccount: [NSString stringWithFormat: @"%@-piggy", key.uuid], | |
453 | (id)kSecAttrServer: key.zoneID.zoneName, | |
454 | (id)kSecReturnAttributes: @YES, | |
455 | (id)kSecReturnData: @YES, | |
456 | (id)kSecMatchLimit: (id)kSecMatchLimitOne, | |
457 | } mutableCopy]; | |
458 | status = SecItemCopyMatching((__bridge CFDictionaryRef) query, &result); | |
459 | if(status == errSecSuccess){ | |
460 | secnotice("ckkskey", "loaded a piggy TLK (%@)", key.uuid); | |
461 | ||
462 | if(resavePtr) { | |
463 | *resavePtr = true; | |
464 | } | |
465 | } | |
466 | } | |
467 | } | |
468 | if(status == errSecItemNotFound && [key.keyclass isEqualToString: SecCKKSKeyClassTLK]) { | |
469 | CFReleaseNull(result); | |
470 | ||
471 | // Try to look for the non-syncable stashed tlk and resurrect it. | |
472 | query = [@{ | |
473 | (id)kSecClass : (id)kSecClassInternetPassword, | |
474 | (id)kSecAttrNoLegacy : @YES, | |
475 | (id)kSecAttrAccessGroup : @"com.apple.security.ckks", | |
476 | (id)kSecAttrDescription: [key.keyclass stringByAppendingString: @"-nonsync"], | |
477 | (id)kSecAttrServer: key.zoneID.zoneName, | |
478 | (id)kSecAttrAccount: key.uuid, | |
479 | (id)kSecReturnAttributes: @YES, | |
480 | (id)kSecReturnData: @YES, | |
481 | (id)kSecAttrSynchronizable: @NO, | |
482 | } mutableCopy]; | |
483 | ||
484 | status = SecItemCopyMatching((__bridge CFDictionaryRef) query, &result); | |
485 | if(status == errSecSuccess) { | |
486 | secnotice("ckkskey", "loaded a stashed TLK (%@)", key.uuid); | |
487 | ||
488 | if(resavePtr) { | |
489 | *resavePtr = true; | |
490 | } | |
491 | } | |
492 | } | |
493 | ||
494 | if(status){ //still can't find it! | |
495 | if(error) { | |
496 | *error = [NSError errorWithDomain:@"securityd" | |
497 | code:status | |
498 | userInfo:@{NSLocalizedDescriptionKey: | |
499 | [NSString stringWithFormat:@"Couldn't load %@ from keychain: %d", self, (int)status]}]; | |
500 | } | |
501 | return false; | |
502 | } | |
503 | ||
504 | // Determine if we should fix up any attributes on this item... | |
505 | NSDictionary* resultDict = CFBridgingRelease(result); | |
506 | ||
507 | // We created some TLKs with no ViewHint. Fix it. | |
508 | if([key.keyclass isEqualToString: SecCKKSKeyClassTLK]) { | |
509 | NSString* viewHint = resultDict[(id)kSecAttrSyncViewHint]; | |
510 | if(!viewHint) { | |
511 | ckksnotice("ckkskey", key.zoneID, "Fixing up non-viewhinted TLK %@", self); | |
512 | query[(id)kSecReturnAttributes] = nil; | |
513 | query[(id)kSecReturnData] = nil; | |
514 | ||
515 | NSDictionary* update = @{(id)kSecAttrSyncViewHint: (id)kSecAttrViewHintPCSMasterKey}; | |
516 | ||
517 | status = SecItemUpdate((__bridge CFDictionaryRef)query, (__bridge CFDictionaryRef)update); | |
518 | if(status) { | |
519 | // Don't report error upwards; this is an optimization fixup. | |
520 | secerror("ckkskey: Couldn't update viewhint on existing TLK %@", self); | |
521 | } | |
522 | } | |
523 | } | |
524 | ||
525 | // Okay, back to the real purpose of this function: extract the CFData currently in the results dictionary | |
526 | NSData* b64keymaterial = resultDict[(id)kSecValueData]; | |
527 | NSData* keymaterial = [[NSData alloc] initWithBase64EncodedData:b64keymaterial options:0]; | |
528 | return keymaterial; | |
529 | } | |
530 | ||
531 | - (bool)loadKeyMaterialFromKeychain: (NSError * __autoreleasing *) error { | |
532 | bool resave = false; | |
533 | NSData* keymaterial = [CKKSKey loadKeyMaterialFromKeychain:self resave:&resave error:error]; | |
534 | if(!keymaterial) { | |
535 | return false; | |
536 | } | |
537 | ||
538 | CKKSAESSIVKey* key = [[CKKSAESSIVKey alloc] initWithBytes: (uint8_t*) keymaterial.bytes len:keymaterial.length]; | |
539 | _aessivkey = key; | |
540 | ||
541 | if(resave) { | |
542 | secnotice("ckkskey", "Resaving %@ as per request", self); | |
543 | NSError* resaveError = nil; | |
544 | [self saveKeyMaterialToKeychain:&resaveError]; | |
545 | if(resaveError) { | |
546 | secnotice("ckkskey", "Resaving %@ failed: %@", self, resaveError); | |
547 | } | |
548 | } | |
549 | ||
550 | return !!(self.aessivkey); | |
551 | } | |
552 | ||
553 | - (bool)deleteKeyMaterialFromKeychain: (NSError * __autoreleasing *) error { | |
554 | ||
555 | NSMutableDictionary* query = [@{ | |
556 | (id)kSecClass : (id)kSecClassInternetPassword, | |
557 | (id)kSecAttrNoLegacy : @YES, | |
558 | (id)kSecAttrAccessGroup : @"com.apple.security.ckks", | |
559 | (id)kSecAttrDescription: self.keyclass, | |
560 | (id)kSecAttrAccount: self.uuid, | |
561 | (id)kSecAttrServer: self.zoneID.zoneName, | |
562 | (id)kSecReturnData: @YES, | |
563 | } mutableCopy]; | |
564 | ||
565 | // Synchronizable items are only found if you request synchronizable items. Only TLKs are synchronizable. | |
566 | if([self.keyclass isEqualToString: SecCKKSKeyClassTLK]) { | |
567 | query[(id)kSecAttrSynchronizable] = (id)kCFBooleanTrue; | |
568 | } | |
569 | ||
570 | OSStatus status = SecItemDelete((__bridge CFDictionaryRef) query); | |
571 | ||
572 | if(status) { | |
573 | if(error) { | |
574 | *error = [NSError errorWithDomain:@"securityd" | |
575 | code:status | |
576 | userInfo:@{NSLocalizedDescriptionKey: | |
577 | [NSString stringWithFormat:@"Couldn't delete %@ from keychain: %d", self, (int)status]}]; | |
578 | } | |
579 | return false; | |
580 | } | |
581 | return true; | |
582 | } | |
583 | ||
584 | + (instancetype)keyFromKeychain: (NSString*) uuid | |
585 | parentKeyUUID: (NSString*) parentKeyUUID | |
586 | keyclass: (CKKSKeyClass*)keyclass | |
587 | state: (CKKSProcessedState*) state | |
588 | zoneID: (CKRecordZoneID*) zoneID | |
589 | encodedCKRecord: (NSData*) encodedrecord | |
590 | currentkey: (NSInteger) currentkey | |
591 | error: (NSError * __autoreleasing *) error { | |
592 | CKKSKey* key = [[CKKSKey alloc] initWithWrappedAESKey:nil | |
593 | uuid:uuid | |
594 | parentKeyUUID:parentKeyUUID | |
595 | keyclass:keyclass | |
596 | state:state | |
597 | zoneID:zoneID | |
598 | encodedCKRecord:encodedrecord | |
599 | currentkey:currentkey]; | |
600 | ||
601 | if(![key loadKeyMaterialFromKeychain:error]) { | |
602 | return nil; | |
603 | } | |
604 | ||
605 | return key; | |
606 | } | |
607 | ||
608 | + (NSString*)isItemKeyForKeychainView: (SecDbItemRef) item { | |
609 | ||
610 | NSString* accessgroup = (__bridge NSString*) SecDbItemGetCachedValueWithName(item, kSecAttrAccessGroup); | |
611 | NSString* description = (__bridge NSString*) SecDbItemGetCachedValueWithName(item, kSecAttrDescription); | |
612 | NSString* server = (__bridge NSString*) SecDbItemGetCachedValueWithName(item, kSecAttrServer); | |
613 | ||
614 | if(accessgroup && description && server && | |
615 | ![accessgroup isEqual:[NSNull null]] && | |
616 | ![description isEqual:[NSNull null]] && | |
617 | ![server isEqual:[NSNull null]] && | |
618 | ||
619 | [accessgroup isEqualToString:@"com.apple.security.ckks"] && | |
620 | ([description isEqualToString: SecCKKSKeyClassTLK] || | |
621 | [description isEqualToString: [NSString stringWithFormat:@"%@-nonsync", SecCKKSKeyClassTLK]] || | |
622 | [description isEqualToString: [NSString stringWithFormat:@"%@-piggy", SecCKKSKeyClassTLK]] || | |
623 | [description isEqualToString: SecCKKSKeyClassA] || | |
624 | [description isEqualToString: SecCKKSKeyClassC])) { | |
625 | ||
626 | // Certainly looks like us! Return the view name. | |
627 | return server; | |
628 | } | |
629 | ||
630 | // Never heard of this item. | |
631 | return nil; | |
632 | } | |
633 | ||
634 | ||
635 | /* Database functions only return keys marked 'local', unless otherwise specified. */ | |
636 | ||
637 | + (instancetype) fromDatabase: (NSString*) uuid zoneID:(CKRecordZoneID*)zoneID error: (NSError * __autoreleasing *) error { | |
638 | return [self fromDatabaseWhere: @{@"UUID": uuid, @"state": SecCKKSProcessedStateLocal, @"ckzone":zoneID.zoneName} error: error]; | |
639 | } | |
640 | ||
641 | + (instancetype) fromDatabaseAnyState: (NSString*) uuid zoneID:(CKRecordZoneID*)zoneID error: (NSError * __autoreleasing *) error { | |
642 | return [self fromDatabaseWhere: @{@"UUID": uuid, @"ckzone":zoneID.zoneName} error: error]; | |
643 | } | |
644 | ||
645 | + (instancetype) tryFromDatabase: (NSString*) uuid zoneID:(CKRecordZoneID*)zoneID error: (NSError * __autoreleasing *) error { | |
646 | return [self tryFromDatabaseWhere: @{@"UUID": uuid, @"state": SecCKKSProcessedStateLocal, @"ckzone":zoneID.zoneName} error: error]; | |
647 | } | |
648 | ||
649 | + (instancetype) tryFromDatabaseAnyState: (NSString*) uuid zoneID:(CKRecordZoneID*)zoneID error: (NSError * __autoreleasing *) error { | |
650 | return [self tryFromDatabaseWhere: @{@"UUID": uuid, @"ckzone":zoneID.zoneName} error: error]; | |
651 | } | |
652 | ||
653 | + (NSArray<CKKSKey*>*)selfWrappedKeys:(CKRecordZoneID*)zoneID error: (NSError * __autoreleasing *) error { | |
654 | return [self allWhere: @{@"UUID": [CKKSSQLWhereObject op:@"=" string:@"parentKeyUUID"], @"state": SecCKKSProcessedStateLocal, @"ckzone":zoneID.zoneName} error:error]; | |
655 | } | |
656 | ||
657 | + (instancetype) currentKeyForClass: (CKKSKeyClass*) keyclass zoneID:(CKRecordZoneID*)zoneID error: (NSError * __autoreleasing *) error { | |
658 | // Load the CurrentKey record, and find the key for it | |
659 | CKKSCurrentKeyPointer* ckp = [CKKSCurrentKeyPointer fromDatabase:keyclass zoneID:zoneID error:error]; | |
660 | if(!ckp) { | |
661 | return nil; | |
662 | } | |
663 | return [self fromDatabase:ckp.currentKeyUUID zoneID:zoneID error:error]; | |
664 | } | |
665 | ||
666 | + (NSArray<CKKSKey*>*) currentKeysForClass: (CKKSKeyClass*) keyclass state:(NSString*) state zoneID:(CKRecordZoneID*)zoneID error: (NSError * __autoreleasing *) error { | |
667 | return [self allWhere: @{@"keyclass": keyclass, @"currentkey": @"1", @"state": state ? state : SecCKKSProcessedStateLocal, @"ckzone":zoneID.zoneName} error:error]; | |
668 | } | |
669 | ||
670 | /* Returns all keys for a zone */ | |
671 | + (NSArray<CKKSKey*>*)allKeys: (CKRecordZoneID*)zoneID error: (NSError * __autoreleasing *) error { | |
672 | return [self allWhere: @{@"ckzone":zoneID.zoneName} error:error]; | |
673 | } | |
674 | ||
675 | /* Returns all keys marked 'remote', i.e., downloaded from CloudKit */ | |
676 | + (NSArray<CKKSKey*>*)remoteKeys: (CKRecordZoneID*)zoneID error: (NSError * __autoreleasing *) error { | |
677 | return [self allWhere: @{@"state": SecCKKSProcessedStateRemote, @"ckzone":zoneID.zoneName} error:error]; | |
678 | } | |
679 | ||
680 | /* Returns all keys marked 'local', i.e., processed in the past */ | |
681 | + (NSArray<CKKSKey*>*)localKeys: (CKRecordZoneID*)zoneID error: (NSError * __autoreleasing *) error { | |
682 | return [self allWhere: @{@"state": SecCKKSProcessedStateLocal, @"ckzone":zoneID.zoneName} error:error]; | |
683 | } | |
684 | ||
685 | - (bool)saveToDatabaseAsOnlyCurrentKeyForClassAndState: (NSError * __autoreleasing *) error { | |
686 | self.currentkey = true; | |
687 | ||
688 | // Find other keys for our key class | |
689 | NSArray<CKKSKey*>* keys = [CKKSKey currentKeysForClass: self.keyclass state: self.state zoneID:self.zoneID error:error]; | |
690 | if(!keys) { | |
691 | return false; | |
692 | } | |
693 | ||
694 | for(CKKSKey* key in keys) { | |
695 | key.currentkey = false; | |
696 | if(![key saveToDatabase: error]) { | |
697 | return false; | |
698 | } | |
699 | } | |
700 | if(![self saveToDatabase: error]) { | |
701 | return false; | |
702 | } | |
703 | ||
704 | return true; | |
705 | } | |
706 | ||
707 | #pragma mark - CKRecord handling | |
708 | ||
709 | - (void) setFromCKRecord: (CKRecord*) record { | |
710 | if(![record.recordType isEqual: SecCKRecordIntermediateKeyType]) { | |
711 | @throw [NSException | |
712 | exceptionWithName:@"WrongCKRecordTypeException" | |
713 | reason:[NSString stringWithFormat: @"CKRecordType (%@) was not %@", record.recordType, SecCKRecordIntermediateKeyType] | |
714 | userInfo:nil]; | |
715 | } | |
716 | ||
717 | [self setStoredCKRecord: record]; | |
718 | ||
719 | self.uuid = [[record recordID] recordName]; | |
720 | if(record[SecCKRecordParentKeyRefKey] != nil) { | |
721 | self.parentKeyUUID = [record[SecCKRecordParentKeyRefKey] recordID].recordName; | |
722 | } else { | |
723 | // We wrap ourself. | |
724 | self.parentKeyUUID = self.uuid; | |
725 | } | |
726 | ||
727 | self.keyclass = record[SecCKRecordKeyClassKey]; | |
728 | self.wrappedkey = [[CKKSWrappedAESSIVKey alloc] initWithBase64: record[SecCKRecordWrappedKeyKey]]; | |
729 | } | |
730 | ||
731 | - (CKRecord*) updateCKRecord: (CKRecord*) record zoneID: (CKRecordZoneID*) zoneID { | |
732 | if(![record.recordType isEqual: SecCKRecordIntermediateKeyType]) { | |
733 | @throw [NSException | |
734 | exceptionWithName:@"WrongCKRecordTypeException" | |
735 | reason:[NSString stringWithFormat: @"CKRecordType (%@) was not %@", record.recordType, SecCKRecordIntermediateKeyType] | |
736 | userInfo:nil]; | |
737 | } | |
738 | ||
739 | // The parent key must exist in CloudKit, or this record save will fail. | |
740 | if([self.parentKeyUUID isEqual: self.uuid]) { | |
741 | // We wrap ourself. No parent. | |
742 | record[SecCKRecordParentKeyRefKey] = nil; | |
743 | } else { | |
744 | record[SecCKRecordParentKeyRefKey] = [[CKReference alloc] initWithRecordID: [[CKRecordID alloc] initWithRecordName: self.parentKeyUUID zoneID: zoneID] action: CKReferenceActionValidate]; | |
745 | } | |
746 | ||
747 | [CKKSItem setOSVersionInRecord: record]; | |
748 | ||
749 | record[SecCKRecordKeyClassKey] = self.keyclass; | |
750 | record[SecCKRecordWrappedKeyKey] = [self.wrappedkey base64WrappedKey]; | |
751 | ||
752 | return record; | |
753 | } | |
754 | ||
755 | #pragma mark - Utility | |
756 | ||
757 | - (NSString*)description { | |
758 | return [NSString stringWithFormat: @"<%@(%@): %@ (%@,%@:%d) %p>", | |
759 | NSStringFromClass([self class]), | |
760 | self.zoneID.zoneName, | |
761 | self.uuid, | |
762 | self.keyclass, | |
763 | self.state, | |
764 | self.currentkey, | |
765 | &self]; | |
766 | } | |
767 | ||
768 | #pragma mark - CKKSSQLDatabaseObject methods | |
769 | ||
770 | + (NSString*) sqlTable { | |
771 | return @"synckeys"; | |
772 | } | |
773 | ||
774 | + (NSArray<NSString*>*) sqlColumns { | |
775 | return @[@"UUID", @"parentKeyUUID", @"ckzone", @"ckrecord", @"keyclass", @"state", @"currentkey", @"wrappedkey"]; | |
776 | } | |
777 | ||
778 | - (NSDictionary<NSString*,NSString*>*) whereClauseToFindSelf { | |
779 | return @{@"UUID": self.uuid, @"state": self.state, @"ckzone":self.zoneID.zoneName}; | |
780 | } | |
781 | ||
782 | - (NSDictionary<NSString*,NSString*>*) sqlValues { | |
783 | return @{@"UUID": self.uuid, | |
784 | @"parentKeyUUID": self.parentKeyUUID ? self.parentKeyUUID : self.uuid, // if we don't have a parent, we wrap ourself. | |
785 | @"ckzone": CKKSNilToNSNull(self.zoneID.zoneName), | |
786 | @"ckrecord": CKKSNilToNSNull([self.encodedCKRecord base64EncodedStringWithOptions:0]), | |
787 | @"keyclass": CKKSNilToNSNull(self.keyclass), | |
788 | @"state": CKKSNilToNSNull(self.state), | |
789 | @"wrappedkey": CKKSNilToNSNull([self.wrappedkey base64WrappedKey]), | |
790 | @"currentkey": self.currentkey ? @"1" : @"0"}; | |
791 | } | |
792 | ||
793 | + (instancetype) fromDatabaseRow: (NSDictionary*) row { | |
794 | return [[CKKSKey alloc] initWithWrappedAESKey: row[@"wrappedkey"] ? [[CKKSWrappedAESSIVKey alloc] initWithBase64: row[@"wrappedkey"]] : nil | |
795 | uuid: row[@"UUID"] | |
796 | parentKeyUUID: row[@"parentKeyUUID"] | |
797 | keyclass: row[@"keyclass"] | |
798 | state: row[@"state"] | |
799 | zoneID: [[CKRecordZoneID alloc] initWithZoneName: row[@"ckzone"] ownerName:CKCurrentUserDefaultName] | |
800 | encodedCKRecord: [[NSData alloc] initWithBase64EncodedString: row[@"ckrecord"] options:0] | |
801 | currentkey: [row[@"currentkey"] integerValue]]; | |
802 | ||
803 | } | |
804 | ||
805 | + (NSDictionary<NSString*,NSNumber*>*)countsByClass:(CKRecordZoneID*)zoneID error: (NSError * __autoreleasing *) error { | |
806 | NSMutableDictionary* results = [[NSMutableDictionary alloc] init]; | |
807 | ||
808 | [CKKSSQLDatabaseObject queryDatabaseTable: [[self class] sqlTable] | |
809 | where: @{@"ckzone": CKKSNilToNSNull(zoneID.zoneName)} | |
810 | columns: @[@"keyclass", @"state", @"count(rowid)"] | |
811 | groupBy: @[@"keyclass", @"state"] | |
812 | orderBy:nil | |
813 | limit: -1 | |
814 | processRow: ^(NSDictionary* row) { | |
815 | results[[NSString stringWithFormat: @"%@-%@", row[@"state"], row[@"keyclass"]]] = | |
816 | [NSNumber numberWithInteger: [row[@"count(rowid)"] integerValue]]; | |
817 | } | |
818 | error: error]; | |
819 | return results; | |
820 | } | |
821 | ||
822 | - (instancetype)copyWithZone:(NSZone *)zone { | |
823 | CKKSKey *keyCopy = [super copyWithZone:zone]; | |
824 | keyCopy->_aessivkey = _aessivkey; | |
825 | keyCopy->_state = _state; | |
826 | keyCopy->_keyclass = _keyclass; | |
827 | keyCopy->_currentkey = _currentkey; | |
828 | return keyCopy; | |
829 | } | |
830 | ||
831 | @end | |
832 | ||
833 | #endif // OCTAGON |