]>
Commit | Line | Data |
---|---|---|
b54c578e A |
1 | |
2 | #if OCTAGON | |
3 | ||
4 | #import "keychain/ckks/CKKSKeychainBackedKey.h" | |
5 | ||
6 | #include <CloudKit/CloudKit.h> | |
7 | #include <CloudKit/CloudKit_Private.h> | |
8 | #include <Security/SecItem.h> | |
9 | #include <Security/SecItemPriv.h> | |
10 | ||
11 | #import "keychain/categories/NSError+UsefulConstructors.h" | |
12 | #import "keychain/ckks/CKKS.h" | |
13 | #import "keychain/ckks/CKKSItem.h" | |
14 | ||
15 | @implementation CKKSKeychainBackedKey | |
16 | ||
17 | - (instancetype)initSelfWrappedWithAESKey:(CKKSAESSIVKey*)aeskey | |
18 | uuid:(NSString*)uuid | |
19 | keyclass:(CKKSKeyClass*)keyclass | |
20 | zoneID:(CKRecordZoneID*)zoneID | |
21 | { | |
22 | if((self = [super init])) { | |
23 | _uuid = uuid; | |
24 | _parentKeyUUID = uuid; | |
25 | _zoneID = zoneID; | |
26 | ||
27 | _keyclass = keyclass; | |
28 | _aessivkey = aeskey; | |
29 | ||
30 | // Wrap the key with the key. Not particularly useful, but there you go. | |
31 | NSError* error = nil; | |
32 | [self wrapUnder:self error:&error]; | |
33 | if(error != nil) { | |
34 | secerror("CKKSKeychainBackedKey: Couldn't self-wrap key: %@", error); | |
35 | return nil; | |
36 | } | |
37 | } | |
38 | return self; | |
39 | } | |
40 | ||
41 | - (instancetype _Nullable)initWrappedBy:(CKKSKeychainBackedKey*)wrappingKey | |
42 | AESKey:(CKKSAESSIVKey*)aessivkey | |
43 | uuid:(NSString*)uuid | |
44 | keyclass:(CKKSKeyClass*)keyclass | |
45 | zoneID:(CKRecordZoneID*)zoneID | |
46 | { | |
47 | if((self = [super init])) { | |
48 | _uuid = uuid; | |
49 | _parentKeyUUID = uuid; | |
50 | _zoneID = zoneID; | |
51 | ||
52 | _keyclass = keyclass; | |
53 | _aessivkey = aessivkey; | |
54 | ||
55 | NSError* error = nil; | |
56 | [self wrapUnder:wrappingKey error:&error]; | |
57 | if(error != nil) { | |
58 | secerror("CKKSKeychainBackedKey: Couldn't wrap key with key: %@", error); | |
59 | return nil; | |
60 | } | |
61 | } | |
62 | return self; | |
63 | } | |
64 | ||
65 | - (instancetype)initWithWrappedAESKey:(CKKSWrappedAESSIVKey*)wrappedaeskey | |
66 | uuid:(NSString*)uuid | |
67 | parentKeyUUID:(NSString*)parentKeyUUID | |
68 | keyclass:(CKKSKeyClass*)keyclass | |
69 | zoneID:(CKRecordZoneID*)zoneID | |
70 | { | |
71 | if((self = [super init])) { | |
72 | _uuid = uuid; | |
73 | _parentKeyUUID = parentKeyUUID; | |
74 | _zoneID = zoneID; | |
75 | ||
76 | _wrappedkey = wrappedaeskey; | |
77 | ||
78 | _keyclass = keyclass; | |
79 | _aessivkey = nil; | |
80 | } | |
81 | return self; | |
82 | } | |
83 | ||
84 | - (instancetype)copyWithZone:(NSZone*)zone | |
85 | { | |
86 | CKKSKeychainBackedKey* c = | |
87 | [[CKKSKeychainBackedKey allocWithZone:zone] initWithWrappedAESKey:self.wrappedkey | |
88 | uuid:self.uuid | |
89 | parentKeyUUID:self.parentKeyUUID | |
90 | keyclass:self.keyclass | |
91 | zoneID:self.zoneID]; | |
92 | c.aessivkey = [self.aessivkey copy]; | |
93 | return c; | |
94 | } | |
95 | ||
96 | ||
97 | - (BOOL)isEqual:(id)object | |
98 | { | |
99 | if(![object isKindOfClass:[CKKSKeychainBackedKey class]]) { | |
100 | return NO; | |
101 | } | |
102 | ||
103 | CKKSKeychainBackedKey* obj = (CKKSKeychainBackedKey*)object; | |
104 | ||
105 | return ([self.uuid isEqual:obj.uuid] && [self.parentKeyUUID isEqual:obj.parentKeyUUID] && | |
106 | [self.zoneID isEqual:obj.zoneID] && [self.wrappedkey isEqual:obj.wrappedkey] && | |
107 | [self.keyclass isEqual:obj.keyclass] && true) | |
108 | ? YES | |
109 | : NO; | |
110 | } | |
111 | ||
112 | - (bool)wrapsSelf | |
113 | { | |
114 | return [self.uuid isEqual:self.parentKeyUUID]; | |
115 | } | |
116 | ||
117 | - (bool)wrapUnder:(CKKSKeychainBackedKey*)wrappingKey | |
118 | error:(NSError* __autoreleasing*)error | |
119 | { | |
120 | NSError* localError = nil; | |
121 | self.wrappedkey = [wrappingKey wrapAESKey:self.aessivkey error:&localError]; | |
122 | if(self.wrappedkey == nil) { | |
123 | secerror("CKKSKeychainBackedKey: couldn't wrap key: %@", localError); | |
124 | if(error) { | |
125 | *error = localError; | |
126 | } | |
127 | } else { | |
128 | self.parentKeyUUID = wrappingKey.uuid; | |
129 | } | |
130 | return (self.wrappedkey != nil); | |
131 | } | |
132 | ||
133 | - (bool)unwrapSelfWithAESKey:(CKKSAESSIVKey*)unwrappingKey | |
134 | error:(NSError* __autoreleasing*)error | |
135 | { | |
136 | _aessivkey = [unwrappingKey unwrapAESKey:self.wrappedkey error:error]; | |
137 | return (_aessivkey != nil); | |
138 | } | |
139 | ||
140 | + (CKKSKeychainBackedKey* _Nullable)randomKeyWrappedByParent:(CKKSKeychainBackedKey*)parentKey | |
141 | error:(NSError* __autoreleasing*)error | |
142 | { | |
143 | return | |
144 | [self randomKeyWrappedByParent:parentKey keyclass:parentKey.keyclass error:error]; | |
145 | } | |
146 | ||
147 | + (CKKSKeychainBackedKey* _Nullable)randomKeyWrappedByParent:(CKKSKeychainBackedKey*)parentKey | |
148 | keyclass:(CKKSKeyClass*)keyclass | |
149 | error:(NSError* __autoreleasing*)error | |
150 | { | |
151 | CKKSAESSIVKey* aessivkey = [CKKSAESSIVKey randomKey:error]; | |
152 | if(aessivkey == nil) { | |
153 | return nil; | |
154 | } | |
155 | ||
156 | CKKSKeychainBackedKey* key = | |
157 | [[CKKSKeychainBackedKey alloc] initWrappedBy:parentKey | |
158 | AESKey:aessivkey | |
159 | uuid:[[NSUUID UUID] UUIDString] | |
160 | keyclass:keyclass | |
161 | zoneID:parentKey.zoneID]; | |
162 | return key; | |
163 | } | |
164 | ||
165 | + (instancetype _Nullable)randomKeyWrappedBySelf:(CKRecordZoneID*)zoneID | |
166 | error:(NSError* __autoreleasing*)error | |
167 | { | |
168 | CKKSAESSIVKey* aessivkey = [CKKSAESSIVKey randomKey:error]; | |
169 | if(aessivkey == nil) { | |
170 | return nil; | |
171 | } | |
172 | ||
173 | NSString* uuid = [[NSUUID UUID] UUIDString]; | |
174 | ||
175 | CKKSKeychainBackedKey* key = | |
176 | [[CKKSKeychainBackedKey alloc] initSelfWrappedWithAESKey:aessivkey | |
177 | uuid:uuid | |
178 | keyclass:SecCKKSKeyClassTLK | |
179 | zoneID:zoneID]; | |
180 | return key; | |
181 | } | |
182 | ||
183 | - (CKKSAESSIVKey*)ensureKeyLoaded:(NSError* __autoreleasing*)error | |
184 | { | |
185 | if(self.aessivkey) { | |
186 | return self.aessivkey; | |
187 | } | |
188 | ||
189 | // Attempt to load this key from the keychain | |
190 | if([self loadKeyMaterialFromKeychain:error]) { | |
191 | return self.aessivkey; | |
192 | } | |
193 | ||
194 | return nil; | |
195 | } | |
196 | ||
197 | - (bool)trySelfWrappedKeyCandidate:(CKKSAESSIVKey*)candidate | |
198 | error:(NSError* __autoreleasing*)error | |
199 | { | |
200 | if(![self wrapsSelf]) { | |
201 | if(error) { | |
202 | *error = [NSError errorWithDomain:CKKSErrorDomain | |
203 | code:CKKSKeyNotSelfWrapped | |
204 | userInfo:@{ | |
205 | NSLocalizedDescriptionKey : [NSString | |
206 | stringWithFormat:@"%@ is not self-wrapped", self] | |
207 | }]; | |
208 | } | |
209 | return false; | |
210 | } | |
211 | ||
212 | CKKSAESSIVKey* unwrapped = [candidate unwrapAESKey:self.wrappedkey error:error]; | |
213 | if(unwrapped && [unwrapped isEqual:candidate]) { | |
214 | _aessivkey = unwrapped; | |
215 | return true; | |
216 | } | |
217 | ||
218 | return false; | |
219 | } | |
220 | ||
221 | - (CKKSWrappedAESSIVKey*)wrapAESKey:(CKKSAESSIVKey*)keyToWrap | |
222 | error:(NSError* __autoreleasing*)error | |
223 | { | |
224 | CKKSAESSIVKey* key = [self ensureKeyLoaded:error]; | |
225 | CKKSWrappedAESSIVKey* wrappedkey = [key wrapAESKey:keyToWrap error:error]; | |
226 | return wrappedkey; | |
227 | } | |
228 | ||
229 | - (CKKSAESSIVKey*)unwrapAESKey:(CKKSWrappedAESSIVKey*)keyToUnwrap | |
230 | error:(NSError* __autoreleasing*)error | |
231 | { | |
232 | CKKSAESSIVKey* key = [self ensureKeyLoaded:error]; | |
233 | CKKSAESSIVKey* unwrappedkey = [key unwrapAESKey:keyToUnwrap error:error]; | |
234 | return unwrappedkey; | |
235 | } | |
236 | ||
237 | - (NSData*)encryptData:(NSData*)plaintext | |
238 | authenticatedData:(NSDictionary<NSString*, NSData*>*)ad | |
239 | error:(NSError* __autoreleasing*)error | |
240 | { | |
241 | CKKSAESSIVKey* key = [self ensureKeyLoaded:error]; | |
242 | NSData* data = [key encryptData:plaintext authenticatedData:ad error:error]; | |
243 | return data; | |
244 | } | |
245 | ||
246 | - (NSData*)decryptData:(NSData*)ciphertext | |
247 | authenticatedData:(NSDictionary<NSString*, NSData*>*)ad | |
248 | error:(NSError* __autoreleasing*)error | |
249 | { | |
250 | CKKSAESSIVKey* key = [self ensureKeyLoaded:error]; | |
251 | NSData* data = [key decryptData:ciphertext authenticatedData:ad error:error]; | |
252 | return data; | |
253 | } | |
254 | ||
255 | /* Functions to load and save keys from the keychain (where we get to store actual key material!) */ | |
256 | - (BOOL)saveKeyMaterialToKeychain:(NSError* __autoreleasing*)error | |
257 | { | |
258 | return [self saveKeyMaterialToKeychain:true error:error]; | |
259 | } | |
260 | ||
261 | - (BOOL)saveKeyMaterialToKeychain:(bool)stashTLK error:(NSError* __autoreleasing*)error | |
262 | { | |
263 | // Note that we only store the key class, view, UUID, parentKeyUUID, and key material in the keychain | |
264 | // Any other metadata must be stored elsewhere and filled in at load time. | |
265 | ||
266 | if(![self ensureKeyLoaded:error]) { | |
267 | // No key material, nothing to save to keychain. | |
268 | return NO; | |
269 | } | |
270 | ||
271 | // iOS keychains can't store symmetric keys, so we're reduced to storing this key as a password | |
272 | NSData* keydata = | |
273 | [[[NSData alloc] initWithBytes:self.aessivkey->key length:self.aessivkey->size] | |
274 | base64EncodedDataWithOptions:0]; | |
275 | NSMutableDictionary* query = [@{ | |
276 | (id)kSecClass : (id)kSecClassInternetPassword, | |
277 | (id)kSecAttrAccessible : (id)kSecAttrAccessibleWhenUnlocked, | |
278 | (id)kSecUseDataProtectionKeychain : @YES, | |
279 | (id)kSecAttrAccessGroup : @"com.apple.security.ckks", | |
280 | (id)kSecAttrDescription : self.keyclass, | |
281 | (id)kSecAttrServer : self.zoneID.zoneName, | |
282 | (id)kSecAttrAccount : self.uuid, | |
283 | (id)kSecAttrPath : self.parentKeyUUID, | |
284 | (id)kSecAttrIsInvisible : @YES, | |
285 | (id)kSecValueData : keydata, | |
286 | } mutableCopy]; | |
287 | ||
288 | // Only TLKs are synchronizable. Other keyclasses must synchronize via key hierarchy. | |
289 | if([self.keyclass isEqualToString:SecCKKSKeyClassTLK]) { | |
290 | // Use PCS-MasterKey view so they'll be initial-synced under SOS. | |
291 | query[(id)kSecAttrSyncViewHint] = (id)kSecAttrViewHintPCSMasterKey; | |
292 | query[(id)kSecAttrSynchronizable] = (id)kCFBooleanTrue; | |
293 | } | |
294 | ||
295 | // Class C keys are accessible after first unlock; TLKs and Class A keys are accessible only when unlocked | |
296 | if([self.keyclass isEqualToString:SecCKKSKeyClassC]) { | |
297 | query[(id)kSecAttrAccessible] = (id)kSecAttrAccessibleAfterFirstUnlock; | |
298 | } else { | |
299 | query[(id)kSecAttrAccessible] = (id)kSecAttrAccessibleWhenUnlocked; | |
300 | } | |
301 | ||
302 | NSError* localError = nil; | |
303 | [CKKSKeychainBackedKey setKeyMaterialInKeychain:query error:&localError]; | |
304 | ||
305 | if(localError && error) { | |
306 | *error = [NSError errorWithDomain:@"securityd" | |
307 | code:localError.code | |
308 | userInfo:@{ | |
309 | NSLocalizedDescriptionKey : | |
310 | [NSString stringWithFormat:@"Couldn't save %@ to keychain: %d", | |
311 | self, | |
312 | (int)localError.code], | |
313 | NSUnderlyingErrorKey : localError, | |
314 | }]; | |
315 | } | |
316 | ||
317 | // TLKs are synchronizable. Stash them nonsyncably nearby. | |
318 | // Don't report errors here. | |
319 | if(stashTLK && [self.keyclass isEqualToString:SecCKKSKeyClassTLK]) { | |
320 | query = [@{ | |
321 | (id)kSecClass : (id)kSecClassInternetPassword, | |
322 | (id)kSecAttrAccessible : (id)kSecAttrAccessibleWhenUnlocked, | |
323 | (id)kSecUseDataProtectionKeychain : @YES, | |
324 | (id)kSecAttrAccessGroup : @"com.apple.security.ckks", | |
325 | (id)kSecAttrDescription : [self.keyclass stringByAppendingString:@"-nonsync"], | |
326 | (id)kSecAttrServer : self.zoneID.zoneName, | |
327 | (id)kSecAttrAccount : self.uuid, | |
328 | (id)kSecAttrPath : self.parentKeyUUID, | |
329 | (id)kSecAttrIsInvisible : @YES, | |
330 | (id)kSecValueData : keydata, | |
331 | } mutableCopy]; | |
332 | query[(id)kSecAttrSynchronizable] = (id)kCFBooleanFalse; | |
333 | ||
334 | NSError* stashError = nil; | |
335 | [CKKSKeychainBackedKey setKeyMaterialInKeychain:query error:&localError]; | |
336 | ||
337 | if(stashError) { | |
338 | secerror("CKKSKeychainBackedKey: Couldn't stash %@ to keychain: %@", self, stashError); | |
339 | } | |
340 | } | |
341 | ||
342 | return (localError == nil) ? YES : NO; | |
343 | } | |
344 | ||
345 | + (NSDictionary*)setKeyMaterialInKeychain:(NSDictionary*)query | |
346 | error:(NSError* __autoreleasing*)error | |
347 | { | |
348 | CFTypeRef result = NULL; | |
349 | OSStatus status = SecItemAdd((__bridge CFDictionaryRef)query, &result); | |
350 | ||
351 | NSError* localerror = nil; | |
352 | ||
353 | // Did SecItemAdd fall over due to an existing item? | |
354 | if(status == errSecDuplicateItem) { | |
355 | // Add every primary key attribute to this find dictionary | |
356 | NSMutableDictionary* findQuery = [[NSMutableDictionary alloc] init]; | |
357 | findQuery[(id)kSecClass] = query[(id)kSecClass]; | |
358 | findQuery[(id)kSecAttrSynchronizable] = query[(id)kSecAttrSynchronizable]; | |
359 | findQuery[(id)kSecAttrSyncViewHint] = query[(id)kSecAttrSyncViewHint]; | |
360 | findQuery[(id)kSecAttrAccessGroup] = query[(id)kSecAttrAccessGroup]; | |
361 | findQuery[(id)kSecAttrAccount] = query[(id)kSecAttrAccount]; | |
362 | findQuery[(id)kSecAttrServer] = query[(id)kSecAttrServer]; | |
363 | findQuery[(id)kSecAttrPath] = query[(id)kSecAttrPath]; | |
364 | findQuery[(id)kSecUseDataProtectionKeychain] = query[(id)kSecUseDataProtectionKeychain]; | |
365 | ||
366 | NSMutableDictionary* updateQuery = [query mutableCopy]; | |
367 | updateQuery[(id)kSecClass] = nil; | |
368 | ||
369 | status = SecItemUpdate((__bridge CFDictionaryRef)findQuery, (__bridge CFDictionaryRef)updateQuery); | |
370 | ||
371 | if(status) { | |
372 | localerror = [NSError | |
373 | errorWithDomain:@"securityd" | |
374 | code:status | |
375 | description:[NSString stringWithFormat:@"SecItemUpdate: %d", (int)status]]; | |
376 | } | |
377 | } else { | |
378 | localerror = [NSError | |
379 | errorWithDomain:@"securityd" | |
380 | code:status | |
381 | description:[NSString stringWithFormat:@"SecItemAdd: %d", (int)status]]; | |
382 | } | |
383 | ||
384 | if(status) { | |
385 | CFReleaseNull(result); | |
386 | ||
387 | if(error) { | |
388 | *error = localerror; | |
389 | } | |
390 | return nil; | |
391 | } | |
392 | ||
393 | NSDictionary* resultDict = CFBridgingRelease(result); | |
394 | return resultDict; | |
395 | } | |
396 | ||
397 | + (NSDictionary*)queryKeyMaterialInKeychain:(NSDictionary*)query | |
398 | error:(NSError* __autoreleasing*)error | |
399 | { | |
400 | CFTypeRef result = NULL; | |
401 | OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &result); | |
402 | ||
403 | if(status) { | |
404 | CFReleaseNull(result); | |
405 | ||
406 | if(error) { | |
407 | *error = [NSError | |
408 | errorWithDomain:@"securityd" | |
409 | code:status | |
410 | userInfo:@{ | |
411 | NSLocalizedDescriptionKey : | |
412 | [NSString stringWithFormat:@"SecItemCopyMatching: %d", (int)status] | |
413 | }]; | |
414 | } | |
415 | return nil; | |
416 | } | |
417 | ||
418 | NSDictionary* resultDict = CFBridgingRelease(result); | |
419 | return resultDict; | |
420 | } | |
421 | ||
422 | + (NSDictionary*)fetchKeyMaterialItemFromKeychain:(CKKSKeychainBackedKey*)key | |
423 | resave:(bool*)resavePtr | |
424 | error:(NSError* __autoreleasing*)error | |
425 | { | |
426 | NSMutableDictionary* query = [@{ | |
427 | (id)kSecClass : (id)kSecClassInternetPassword, | |
428 | (id)kSecUseDataProtectionKeychain : @YES, | |
429 | (id)kSecAttrAccessGroup : @"com.apple.security.ckks", | |
430 | (id)kSecAttrDescription : key.keyclass, | |
431 | (id)kSecAttrAccount : key.uuid, | |
432 | (id)kSecAttrServer : key.zoneID.zoneName, | |
433 | (id)kSecAttrPath : key.parentKeyUUID, | |
434 | (id)kSecReturnAttributes : @YES, | |
435 | (id)kSecReturnData : @YES, | |
436 | } mutableCopy]; | |
437 | ||
438 | // Synchronizable items are only found if you request synchronizable items. Only TLKs are synchronizable. | |
439 | if([key.keyclass isEqualToString:SecCKKSKeyClassTLK]) { | |
440 | query[(id)kSecAttrSynchronizable] = (id)kCFBooleanTrue; | |
441 | } | |
442 | ||
443 | NSError* localError = nil; | |
444 | NSDictionary* result = [self queryKeyMaterialInKeychain:query error:&localError]; | |
445 | NSError* originalError = localError; | |
446 | ||
447 | // If we found the item or errored in some interesting way, return. | |
448 | if(result) { | |
449 | return result; | |
450 | } | |
451 | if(localError && localError.code != errSecItemNotFound) { | |
452 | if(error) { | |
453 | *error = [NSError errorWithDomain:@"securityd" | |
454 | code:localError.code | |
455 | userInfo:@{ | |
456 | NSLocalizedDescriptionKey : | |
457 | [NSString stringWithFormat:@"Couldn't load %@ from keychain: %d", | |
458 | key, | |
459 | (int)localError.code], | |
460 | NSUnderlyingErrorKey : localError, | |
461 | }]; | |
462 | } | |
463 | return result; | |
464 | } | |
465 | localError = nil; | |
466 | ||
467 | if([key.keyclass isEqualToString:SecCKKSKeyClassTLK]) { | |
468 | //didn't find a regular tlk? how about a piggy? | |
469 | query = [@{ | |
470 | (id)kSecClass : (id)kSecClassInternetPassword, | |
471 | (id)kSecUseDataProtectionKeychain : @YES, | |
472 | (id)kSecAttrAccessGroup : @"com.apple.security.ckks", | |
473 | (id)kSecAttrDescription : [key.keyclass stringByAppendingString:@"-piggy"], | |
474 | (id)kSecAttrSynchronizable : (id)kSecAttrSynchronizableAny, | |
475 | (id)kSecAttrAccount : [NSString stringWithFormat:@"%@-piggy", key.uuid], | |
476 | (id)kSecAttrServer : key.zoneID.zoneName, | |
477 | (id)kSecReturnAttributes : @YES, | |
478 | (id)kSecReturnData : @YES, | |
479 | (id)kSecMatchLimit : (id)kSecMatchLimitOne, | |
480 | } mutableCopy]; | |
481 | ||
482 | result = [self queryKeyMaterialInKeychain:query error:&localError]; | |
483 | if(localError == nil) { | |
484 | secnotice("CKKSKeychainBackedKey", "loaded a piggy TLK (%@)", key.uuid); | |
485 | ||
486 | if(resavePtr) { | |
487 | *resavePtr = true; | |
488 | } | |
489 | ||
490 | return result; | |
491 | } | |
492 | ||
493 | if(localError && localError.code != errSecItemNotFound) { | |
494 | if(error) { | |
495 | *error = [NSError errorWithDomain:@"securityd" | |
496 | code:localError.code | |
497 | userInfo:@{ | |
498 | NSLocalizedDescriptionKey : [NSString | |
499 | stringWithFormat:@"Couldn't load %@ from keychain: %d", | |
500 | key, | |
501 | (int)localError.code], | |
502 | NSUnderlyingErrorKey : localError, | |
503 | }]; | |
504 | } | |
505 | return nil; | |
506 | } | |
507 | } | |
508 | ||
509 | localError = nil; | |
510 | ||
511 | // Try to load a stashed TLK | |
512 | if([key.keyclass isEqualToString:SecCKKSKeyClassTLK]) { | |
513 | localError = nil; | |
514 | ||
515 | // Try to look for the non-syncable stashed tlk and resurrect it. | |
516 | query = [@{ | |
517 | (id)kSecClass : (id)kSecClassInternetPassword, | |
518 | (id)kSecUseDataProtectionKeychain : @YES, | |
519 | (id)kSecAttrAccessGroup : @"com.apple.security.ckks", | |
520 | (id)kSecAttrDescription : [key.keyclass stringByAppendingString:@"-nonsync"], | |
521 | (id)kSecAttrServer : key.zoneID.zoneName, | |
522 | (id)kSecAttrAccount : key.uuid, | |
523 | (id)kSecReturnAttributes : @YES, | |
524 | (id)kSecReturnData : @YES, | |
525 | (id)kSecAttrSynchronizable : @NO, | |
526 | } mutableCopy]; | |
527 | ||
528 | result = [self queryKeyMaterialInKeychain:query error:&localError]; | |
529 | if(localError == nil) { | |
530 | secnotice("CKKSKeychainBackedKey", "loaded a stashed TLK (%@)", key.uuid); | |
531 | ||
532 | if(resavePtr) { | |
533 | *resavePtr = true; | |
534 | } | |
535 | ||
536 | return result; | |
537 | } | |
538 | ||
539 | if(localError && localError.code != errSecItemNotFound) { | |
540 | if(error) { | |
541 | *error = [NSError errorWithDomain:@"securityd" | |
542 | code:localError.code | |
543 | userInfo:@{ | |
544 | NSLocalizedDescriptionKey : [NSString | |
545 | stringWithFormat:@"Couldn't load %@ from keychain: %d", | |
546 | key, | |
547 | (int)localError.code], | |
548 | NSUnderlyingErrorKey : localError, | |
549 | }]; | |
550 | } | |
551 | return nil; | |
552 | } | |
553 | } | |
554 | ||
555 | // We didn't early-return. Use whatever error the original fetch produced. | |
556 | if(error) { | |
557 | *error = [NSError errorWithDomain:@"securityd" | |
558 | code:originalError ? originalError.code : errSecParam | |
559 | description:[NSString stringWithFormat:@"Couldn't load %@ from keychain: %d", | |
560 | key, | |
561 | (int)originalError.code] | |
562 | underlying:originalError]; | |
563 | } | |
564 | ||
565 | return result; | |
566 | } | |
567 | ||
568 | - (BOOL)loadKeyMaterialFromKeychain:(NSError* __autoreleasing*)error | |
569 | { | |
570 | bool resave = false; | |
571 | NSDictionary* result = [CKKSKeychainBackedKey fetchKeyMaterialItemFromKeychain:self | |
572 | resave:&resave | |
573 | error:error]; | |
574 | if(!result) { | |
575 | return NO; | |
576 | } | |
577 | ||
578 | NSData* b64keymaterial = result[(id)kSecValueData]; | |
579 | NSMutableData* keymaterial = | |
580 | [[NSMutableData alloc] initWithBase64EncodedData:b64keymaterial options:0]; | |
581 | if(!keymaterial) { | |
582 | secnotice("CKKSKeychainBackedKey", "Unable to unbase64 key: %@", self); | |
583 | if(error) { | |
584 | *error = [NSError | |
585 | errorWithDomain:CKKSErrorDomain | |
586 | code:CKKSKeyUnknownFormat | |
587 | description:[NSString stringWithFormat:@"unable to unbase64 key: %@", self]]; | |
588 | } | |
589 | return NO; | |
590 | } | |
591 | ||
592 | CKKSAESSIVKey* key = [[CKKSAESSIVKey alloc] initWithBytes:(uint8_t*)keymaterial.bytes | |
593 | len:keymaterial.length]; | |
594 | memset_s(keymaterial.mutableBytes, keymaterial.length, 0, keymaterial.length); | |
595 | self.aessivkey = key; | |
596 | ||
597 | if(resave) { | |
598 | secnotice("CKKSKeychainBackedKey", "Resaving %@ as per request", self); | |
599 | NSError* resaveError = nil; | |
600 | [self saveKeyMaterialToKeychain:&resaveError]; | |
601 | if(resaveError) { | |
602 | secnotice("CKKSKeychainBackedKey", "Resaving %@ failed: %@", self, resaveError); | |
603 | } | |
604 | } | |
605 | ||
606 | return !!(self.aessivkey) ? YES : NO; | |
607 | } | |
608 | ||
609 | - (BOOL)deleteKeyMaterialFromKeychain:(NSError* __autoreleasing*)error | |
610 | { | |
611 | NSMutableDictionary* query = [@{ | |
612 | (id)kSecClass : (id)kSecClassInternetPassword, | |
613 | (id)kSecUseDataProtectionKeychain : @YES, | |
614 | (id)kSecAttrAccessGroup : @"com.apple.security.ckks", | |
615 | (id)kSecAttrDescription : self.keyclass, | |
616 | (id)kSecAttrAccount : self.uuid, | |
617 | (id)kSecAttrServer : self.zoneID.zoneName, | |
618 | (id)kSecReturnData : @YES, | |
619 | } mutableCopy]; | |
620 | ||
621 | // Synchronizable items are only found if you request synchronizable items. Only TLKs are synchronizable. | |
622 | if([self.keyclass isEqualToString:SecCKKSKeyClassTLK]) { | |
623 | query[(id)kSecAttrSynchronizable] = (id)kCFBooleanTrue; | |
624 | } | |
625 | ||
626 | OSStatus status = SecItemDelete((__bridge CFDictionaryRef)query); | |
627 | ||
628 | if(status) { | |
629 | if(error) { | |
630 | *error = [NSError | |
631 | errorWithDomain:@"securityd" | |
632 | code:status | |
633 | userInfo:@{ | |
634 | NSLocalizedDescriptionKey : [NSString | |
635 | stringWithFormat:@"Couldn't delete %@ from keychain: %d", self, (int)status] | |
636 | }]; | |
637 | } | |
638 | return NO; | |
639 | } | |
640 | return YES; | |
641 | } | |
642 | ||
643 | + (instancetype _Nullable)keyFromKeychain:(NSString*)uuid | |
644 | parentKeyUUID:(NSString*)parentKeyUUID | |
645 | keyclass:(CKKSKeyClass*)keyclass | |
646 | zoneID:(CKRecordZoneID*)zoneID | |
647 | error:(NSError* __autoreleasing*)error | |
648 | { | |
649 | CKKSKeychainBackedKey* key = [[CKKSKeychainBackedKey alloc] initWithWrappedAESKey:nil | |
650 | uuid:uuid | |
651 | parentKeyUUID:parentKeyUUID | |
652 | keyclass:keyclass | |
653 | zoneID:zoneID]; | |
654 | ||
655 | if(![key loadKeyMaterialFromKeychain:error]) { | |
656 | return nil; | |
657 | } | |
658 | ||
659 | return key; | |
660 | } | |
661 | ||
662 | #pragma mark Utility | |
663 | ||
664 | - (NSString*)description | |
665 | { | |
666 | return [NSString stringWithFormat:@"<%@(%@): %@ (%@)>", | |
667 | NSStringFromClass([self class]), | |
668 | self.zoneID.zoneName, | |
669 | self.uuid, | |
670 | self.keyclass]; | |
671 | } | |
672 | ||
673 | - (NSData*)serializeAsProtobuf:(NSError* __autoreleasing*)error | |
674 | { | |
675 | if(![self ensureKeyLoaded:error]) { | |
676 | return nil; | |
677 | } | |
678 | CKKSSerializedKey* proto = [[CKKSSerializedKey alloc] init]; | |
679 | ||
680 | proto.uuid = self.uuid; | |
681 | proto.zoneName = self.zoneID.zoneName; | |
682 | proto.keyclass = self.keyclass; | |
683 | proto.key = | |
684 | [[NSData alloc] initWithBytes:self.aessivkey->key length:self.aessivkey->size]; | |
685 | ||
686 | return proto.data; | |
687 | } | |
688 | ||
689 | + (CKKSKeychainBackedKey*)loadFromProtobuf:(NSData*)data | |
690 | error:(NSError* __autoreleasing*)error | |
691 | { | |
692 | CKKSSerializedKey* key = [[CKKSSerializedKey alloc] initWithData:data]; | |
693 | if(key && key.uuid && key.zoneName && key.keyclass && key.key) { | |
694 | return [[CKKSKeychainBackedKey alloc] | |
695 | initSelfWrappedWithAESKey:[[CKKSAESSIVKey alloc] | |
696 | initWithBytes:(uint8_t*)key.key.bytes | |
697 | len:key.key.length] | |
698 | uuid:key.uuid | |
699 | keyclass:(CKKSKeyClass*)key.keyclass // TODO sanitize | |
700 | zoneID:[[CKRecordZoneID alloc] initWithZoneName:key.zoneName | |
701 | ownerName:CKCurrentUserDefaultName]]; | |
702 | } | |
703 | ||
704 | if(error) { | |
705 | *error = [NSError errorWithDomain:CKKSErrorDomain | |
706 | code:CKKSProtobufFailure | |
707 | description:@"Data failed to parse as a CKKSSerializedKey"]; | |
708 | } | |
709 | return nil; | |
710 | } | |
711 | ||
712 | #pragma mark NSSecureCoding | |
713 | ||
714 | + (BOOL)supportsSecureCoding | |
715 | { | |
716 | return YES; | |
717 | } | |
718 | ||
719 | - (void)encodeWithCoder:(nonnull NSCoder*)coder | |
720 | { | |
721 | [coder encodeObject:self.uuid forKey:@"uuid"]; | |
722 | [coder encodeObject:self.parentKeyUUID forKey:@"parentKeyUUID"]; | |
723 | [coder encodeObject:self.keyclass forKey:@"keyclass"]; | |
724 | [coder encodeObject:self.zoneID forKey:@"zoneID"]; | |
725 | [coder encodeObject:self.wrappedkey forKey:@"wrappedkey"]; | |
726 | } | |
727 | ||
728 | - (nullable instancetype)initWithCoder:(nonnull NSCoder*)decoder | |
729 | { | |
730 | self = [super init]; | |
731 | if(self) { | |
732 | _uuid = [decoder decodeObjectOfClass:[NSString class] forKey:@"uuid"]; | |
733 | _parentKeyUUID = | |
734 | [decoder decodeObjectOfClass:[NSString class] forKey:@"parentKeyUUID"]; | |
735 | _keyclass = (CKKSKeyClass*)[decoder decodeObjectOfClass:[NSString class] | |
736 | forKey:@"keyclass"]; | |
737 | _zoneID = [decoder decodeObjectOfClass:[CKRecordZoneID class] forKey:@"zoneID"]; | |
738 | ||
739 | _wrappedkey = [decoder decodeObjectOfClass:[CKKSWrappedAESSIVKey class] | |
740 | forKey:@"wrappedkey"]; | |
741 | } | |
742 | return self; | |
743 | } | |
744 | ||
745 | @end | |
746 | ||
747 | #pragma mark - CKKSKeychainBackedKeySet | |
748 | ||
749 | @implementation CKKSKeychainBackedKeySet | |
750 | ||
751 | - (instancetype)initWithTLK:(CKKSKeychainBackedKey*)tlk | |
752 | classA:(CKKSKeychainBackedKey*)classA | |
753 | classC:(CKKSKeychainBackedKey*)classC | |
754 | newUpload:(BOOL)newUpload | |
755 | { | |
756 | if((self = [super init])) { | |
757 | _tlk = tlk; | |
758 | _classA = classA; | |
759 | _classC = classC; | |
760 | _newUpload = newUpload; | |
761 | } | |
762 | return self; | |
763 | } | |
764 | ||
765 | - (NSString*)description | |
766 | { | |
767 | return [NSString stringWithFormat: @"<CKKSKeychainBackedKeySet: tlk:%@, classA:%@, classC:%@, newUpload:%d>", | |
768 | self.tlk, | |
769 | self.classA, | |
770 | self.classC, | |
771 | self.newUpload]; | |
772 | } | |
773 | ||
774 | + (BOOL)supportsSecureCoding | |
775 | { | |
776 | return YES; | |
777 | } | |
778 | ||
779 | - (void)encodeWithCoder:(nonnull NSCoder*)coder | |
780 | { | |
781 | [coder encodeObject:self.tlk forKey:@"tlk"]; | |
782 | [coder encodeObject:self.classA forKey:@"classA"]; | |
783 | [coder encodeObject:self.classC forKey:@"classC"]; | |
784 | [coder encodeBool:self.newUpload forKey:@"newUpload"]; | |
785 | } | |
786 | ||
787 | - (nullable instancetype)initWithCoder:(nonnull NSCoder*)decoder | |
788 | { | |
789 | self = [super init]; | |
790 | if(self) { | |
791 | _tlk = [decoder decodeObjectOfClass:[CKKSKeychainBackedKey class] forKey:@"tlk"]; | |
792 | _classA = [decoder decodeObjectOfClass:[CKKSKeychainBackedKey class] forKey:@"classA"]; | |
793 | _classC = [decoder decodeObjectOfClass:[CKKSKeychainBackedKey class] forKey:@"classC"]; | |
794 | _newUpload = [decoder decodeBoolForKey:@"newUpload"]; | |
795 | } | |
796 | return self; | |
797 | } | |
798 | ||
799 | @end | |
800 | ||
801 | #endif // OCTAGON |