2 * Copyright (c) 2010-2012 Apple Inc. All Rights Reserved.
4 * @APPLE_LICENSE_HEADER_START@
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
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.
21 * @APPLE_LICENSE_HEADER_END@
24 #include "SecRecoveryPassword.h"
25 #include <Security/SecTransform.h>
26 #include <Security/SecEncodeTransform.h>
27 #include <Security/SecDecodeTransform.h>
28 #include <Security/SecDigestTransform.h>
29 #include <Security/SecEncryptTransform.h>
30 #include <Security/SecItem.h>
31 #include <Security/SecKey.h>
32 #include <CommonCrypto/CommonKeyDerivation.h>
33 #include <CommonCrypto/CommonCryptor.h>
34 #include <CoreFoundation/CFBase.h>
40 #include <utilities/SecCFRelease.h>
41 #include <utilities/debugging.h>
43 CFStringRef kSecRecVersionNumber
= CFSTR("SRVersionNumber");
44 CFStringRef kSecRecQuestions
= CFSTR("SRQuestions");
45 CFStringRef kSecRecLocale
= CFSTR("SRLocale");
46 CFStringRef kSecRecIV
= CFSTR("SRiv");
47 CFStringRef kSecRecWrappedPassword
= CFSTR("SRWrappedPassword");
50 static char *std_log_prefix
= "###SecRecovery Function: %s - %s";
51 static const char *std_ident
= "Security.framework";
52 static const char *std_facility
= "InfoSec";
53 static uint32_t std_options
= 0;
55 static aslclient aslhandle
= NULL
;
56 static aslmsg msgptr
= NULL
;
60 void ccdebug_imp(int level
, char *funcname
, char *format
, ...);
62 #define secDebug(lvl,fmt,...) sec_debug_imp(lvl, __PRETTY_FUNCTION__, fmt, __VA_ARGS__)
66 char *ccEnvStdErr
= getenv("CC_STDERR");
68 if(ccEnvStdErr
!= NULL
&& strncmp(ccEnvStdErr
, "yes", 3) == 0) std_options
|= ASL_OPT_STDERR
;
69 aslhandle
= asl_open(std_ident
, std_facility
, std_options
);
71 msgptr
= asl_new(ASL_TYPE_MSG
);
72 asl_set(msgptr
, ASL_KEY_FACILITY
, "com.apple.infosec");
77 sec_debug_imp(int level
, const char *funcname
, char *format
, ...) {
81 if(aslhandle
== NULL
) sec_debug_init();
83 sprintf(fmtbuffer
, std_log_prefix
, funcname
, format
);
84 va_start(argp
, format
);
85 asl_vlog(aslhandle
, msgptr
, level
, fmtbuffer
, argp
);
89 // Read /dev/random for random bytes
92 getRandomBytes(size_t len
)
95 CFDataRef randData
= NULL
;
98 if((buffer
= malloc(len
)) == NULL
) return NULL
;
99 if((fdrand
= open("/dev/random", O_RDONLY
)) == -1) return NULL
;
100 if(read(fdrand
, buffer
, len
) == len
) randData
= CFDataCreate(kCFAllocatorDefault
, (const UInt8
*) buffer
, len
);
107 // This is the normalization routine - subject to change. We need to make sure that whitespace is removed and
108 // that upper/lower case is normalized, etc for all possible languages.
110 static void secNormalize(CFMutableStringRef theString
, CFLocaleRef theLocale
)
114 CFStringFold(theString
, kCFCompareCaseInsensitive
| kCFCompareDiacriticInsensitive
| kCFCompareWidthInsensitive
, theLocale
);
115 CFStringNormalize(theString
, kCFStringNormalizationFormKC
);
116 CFStringTrimWhitespace(theString
);
117 while(CFStringFindCharacterFromSet(theString
, CFCharacterSetGetPredefined(kCFCharacterSetWhitespace
), CFRangeMake(0, CFStringGetLength(theString
)), kCFCompareBackwards
, &theRange
))
118 CFStringDelete(theString
, theRange
);
122 * This will derive a 128 bit (16 byte) key from a set of answers to questions in a CFArray of CFStrings.
123 * it normalizes each answer and concats them into a collector buffer. The resulting string is run through
124 * PBKDF2-HMAC-SHA256 to form a key.
126 * Todo: For version 2 it would be better to randomly generate the salt and make the iteration count flexible.
127 * This would require a different return value because that information would need to be returned up the stack
128 * to the callers. Given the time left in this release (Lion) we're going with set values for this.
131 #define RETURN_KEY_SIZE 16
132 #define MAXANSWERBUFF 4096
133 #define PBKDF_ROUNDS 100000
135 static SecKeyRef
secDeriveKeyFromAnswers(CFArrayRef answers
, CFLocaleRef theLocale
)
137 static const uint8_t salt
[16] = { 0x0A, 0x1F, 0x0A, 0x1F, 0x0A, 0x1F, 0x0A, 0x1F, 0x0A, 0x1F, 0x0A, 0x1F, 0x0A, 0x1F, 0x0A, 0x1F };
138 static const int saltLen
= sizeof(salt
);
140 SecKeyRef theKey
= NULL
;
141 uint8_t rawKeyData
[RETURN_KEY_SIZE
];
143 CFIndex encodedAnswers
= 0;
144 CFIndex numAnswers
= CFArrayGetCount(answers
);
145 const size_t concatenatedAnswersSize
= MAXANSWERBUFF
* numAnswers
;
147 char *concatenatedAnswers
= (char *)malloc(concatenatedAnswersSize
);
148 if (concatenatedAnswers
== NULL
) {
152 concatenatedAnswers
[0] = 0; // NUL terminate
155 for (i
= 0; i
< numAnswers
; i
++) {
156 CFMutableStringRef answer
= CFStringCreateMutableCopy(kCFAllocatorDefault
, 0, CFArrayGetValueAtIndex(answers
, i
));
158 secNormalize(answer
, theLocale
);
160 CFIndex theAnswerLen
= CFStringGetLength(answer
);
161 CFIndex theAnswerSize
= CFStringGetMaximumSizeForEncoding(theAnswerLen
, kCFStringEncodingUTF8
);
162 char *theAnswer
= (char *)malloc(theAnswerSize
+ 1); // add space for NUL byte
164 if (theAnswerLen
== CFStringGetBytes(answer
, CFRangeMake(0, CFStringGetLength(answer
)), kCFStringEncodingUTF8
, '?', FALSE
, (UInt8
*)theAnswer
, theAnswerSize
, &theAnswerSize
)) {
165 theAnswer
[theAnswerSize
] = 0; // NUL terminate
166 if (strlcat(concatenatedAnswers
, theAnswer
, concatenatedAnswersSize
) < concatenatedAnswersSize
) {
170 bzero(theAnswer
, theAnswerSize
);
177 // one or more of the answers failed to encode
178 if (encodedAnswers
!= numAnswers
) {
179 free(concatenatedAnswers
);
183 if (CCKeyDerivationPBKDF(kCCPBKDF2
, concatenatedAnswers
, strlen(concatenatedAnswers
), salt
, saltLen
, kCCPRFHmacAlgSHA256
, PBKDF_ROUNDS
, rawKeyData
, RETURN_KEY_SIZE
)) {
184 free(concatenatedAnswers
);
188 CFDataRef keyData
= CFDataCreate(kCFAllocatorDefault
, rawKeyData
, RETURN_KEY_SIZE
);
190 CFMutableDictionaryRef params
= CFDictionaryCreateMutable(kCFAllocatorDefault
, 1, &kCFTypeDictionaryKeyCallBacks
, &kCFTypeDictionaryValueCallBacks
);
192 CFErrorRef error
= NULL
;
193 CFDictionaryAddValue(params
, kSecAttrKeyType
, kSecAttrKeyTypeAES
);
194 theKey
= SecKeyCreateFromData(params
, keyData
, &error
);
203 bzero(rawKeyData
, RETURN_KEY_SIZE
);
204 bzero(concatenatedAnswers
, concatenatedAnswersSize
);
205 free(concatenatedAnswers
);
210 // Single shot CFString processing routines for digests/encoding/encrypt/decrypt
213 digestString(CFStringRef str
)
215 CFDataRef retval
= NULL
;
216 CFErrorRef error
= NULL
;
218 CFDataRef inputString
= CFStringCreateExternalRepresentation(kCFAllocatorDefault
, str
, kCFStringEncodingUTF8
, 0xff);
220 SecTransformRef digestTrans
= SecDigestTransformCreate(kSecDigestSHA2
, 256, &error
);
222 SecTransformSetAttribute(digestTrans
, kSecTransformInputAttributeName
, inputString
, &error
);
224 retval
= SecTransformExecute(digestTrans
, &error
);
226 secDebug(ASL_LEVEL_ERR
, "Couldn't create digest %s\n", CFStringGetCStringPtr(CFErrorCopyFailureReason(error
), kCFStringEncodingUTF8
));
229 CFRelease(digestTrans
);
231 CFRelease(inputString
);
235 static CFDataRef CF_RETURNS_RETAINED
236 b64encode(CFDataRef input
)
238 CFDataRef retval
= NULL
;
239 CFErrorRef error
= NULL
;
240 SecTransformRef encodeTrans
= SecEncodeTransformCreate(kSecBase64Encoding
, &error
);
241 if(error
== NULL
) SecTransformSetAttribute(encodeTrans
, kSecTransformInputAttributeName
, input
, &error
);
242 if(error
== NULL
) retval
= SecTransformExecute(encodeTrans
, &error
);
243 if(encodeTrans
) CFRelease(encodeTrans
);
247 static CFDataRef CF_RETURNS_RETAINED
248 b64decode(CFDataRef input
)
250 CFDataRef retval
= NULL
;
251 CFErrorRef error
= NULL
;
252 SecTransformRef decodeTrans
= SecDecodeTransformCreate(kSecBase64Encoding
, &error
);
253 if(error
== NULL
) SecTransformSetAttribute(decodeTrans
, kSecTransformInputAttributeName
, input
, &error
);
254 if(error
== NULL
) retval
= SecTransformExecute(decodeTrans
, &error
);
255 if(decodeTrans
) CFRelease(decodeTrans
);
260 encryptString(SecKeyRef wrapKey
, CFDataRef iv
, CFStringRef str
)
262 CFDataRef retval
= NULL
;
263 CFErrorRef error
= NULL
;
264 CFDataRef inputString
= CFStringCreateExternalRepresentation(kCFAllocatorDefault
, str
, kCFStringEncodingMacRoman
, 0xff);
265 SecTransformRef encrypt
= NULL
;
266 SecTransformRef encode
= NULL
;
267 SecTransformRef group
= NULL
;
269 encrypt
= SecEncryptTransformCreate(wrapKey
, &error
);
271 SecTransformSetAttribute(encrypt
, kSecEncryptionMode
, kSecModeCBCKey
, &error
);
273 SecTransformSetAttribute(encrypt
, kSecPaddingKey
, kSecPaddingPKCS7Key
, &error
);
275 SecTransformSetAttribute(encrypt
, kSecTransformInputAttributeName
, inputString
, &error
);
277 SecTransformSetAttribute(encrypt
, kSecIVKey
, iv
, &error
);
280 encode
= SecEncodeTransformCreate(kSecBase64Encoding
, &error
);
283 group
= SecTransformCreateGroupTransform();
284 SecTransformConnectTransforms(encrypt
, kSecTransformOutputAttributeName
, encode
, kSecTransformInputAttributeName
, group
, &error
);
286 retval
= SecTransformExecute(group
, &error
);
291 secerror("Failed to encrypt recovery password: %@", error
);
294 CFReleaseNull(error
);
295 CFReleaseNull(inputString
);
296 CFReleaseNull(encrypt
);
297 CFReleaseNull(encode
);
298 CFReleaseNull(group
);
304 static CFStringRef CF_RETURNS_RETAINED
305 decryptString(SecKeyRef wrapKey
, CFDataRef iv
, CFDataRef wrappedPassword
)
307 CFStringRef retval
= NULL
;
308 CFDataRef retData
= NULL
;
309 CFErrorRef error
= NULL
;
310 SecTransformRef decode
= NULL
;
311 SecTransformRef decrypt
= NULL
;
312 SecTransformRef group
= NULL
;
314 decode
= SecDecodeTransformCreate(kSecBase64Encoding
, &error
);
316 SecTransformSetAttribute(decode
, kSecTransformInputAttributeName
, wrappedPassword
, &error
);
319 decrypt
= SecDecryptTransformCreate(wrapKey
, &error
);
321 SecTransformSetAttribute(decrypt
, kSecEncryptionMode
, kSecModeCBCKey
, &error
);
323 SecTransformSetAttribute(decrypt
, kSecPaddingKey
, kSecPaddingPKCS7Key
, &error
);
325 SecTransformSetAttribute(decrypt
, kSecIVKey
, iv
, &error
);
328 group
= SecTransformCreateGroupTransform();
329 SecTransformConnectTransforms(decode
, kSecTransformOutputAttributeName
, decrypt
, kSecTransformInputAttributeName
, group
, &error
);
331 retData
= SecTransformExecute(group
, &error
);
333 retval
= CFStringCreateFromExternalRepresentation(kCFAllocatorDefault
, retData
, kCFStringEncodingMacRoman
);
337 secerror("Failed to decrypt recovery password: %@", error
);
340 CFReleaseNull(retData
);
341 CFReleaseNull(error
);
342 CFReleaseNull(decode
);
343 CFReleaseNull(decrypt
);
344 CFReleaseNull(group
);
349 // IV for the recovery ref is currently the leftmost 16 bytes of the digest of the recovery password.
350 #define IVBYTECOUNT 16
353 createIVFromPassword(CFStringRef password
)
355 CFDataRef hashedPassword
, retval
;
357 if((hashedPassword
= digestString(password
)) == NULL
) return NULL
;
358 iv
= CFDataCreateMutableCopy(kCFAllocatorDefault
, CFDataGetLength(hashedPassword
)+1, hashedPassword
);
359 CFDataDeleteBytes(iv
, CFRangeMake(IVBYTECOUNT
, CFDataGetLength(iv
)-IVBYTECOUNT
));
360 retval
= CFDataCreateCopy(kCFAllocatorDefault
, iv
);
361 CFRelease(hashedPassword
);
372 * Function: SecWrapRecoveryPasswordWithAnswers
373 * Description: This will wrap a password by using answers to generate a key. The resulting
374 * wrapped password and the questions used to get the answers are saved in a
375 * recovery dictionary.
379 SecWrapRecoveryPasswordWithAnswers(CFStringRef password
, CFArrayRef questions
, CFArrayRef answers
)
383 CFDataRef wrappedPassword
;
384 CFMutableDictionaryRef retval
= NULL
;
385 CFLocaleRef theLocale
= CFLocaleCopyCurrent();
386 CFStringRef theLocaleString
= CFLocaleGetIdentifier(theLocale
);
390 if (!password
|| !questions
|| !answers
)
393 limit
= CFArrayGetCount(answers
);
394 if (limit
!= CFArrayGetCount(questions
))
395 return NULL
; // Error
397 for (ix
=0; ix
<limit
; ix
++)
399 chkval
= CFArrayGetValueAtIndex(answers
, ix
);
400 if (!chkval
|| CFGetTypeID(chkval
)!=CFStringGetTypeID() || CFEqual((CFStringRef
)chkval
, CFSTR("")))
402 chkval
= CFArrayGetValueAtIndex(questions
, ix
);
403 if (!chkval
|| CFGetTypeID(chkval
)!=CFStringGetTypeID() || CFEqual((CFStringRef
)chkval
, CFSTR("")))
407 iv
= createIVFromPassword(password
);
409 SecKeyRef wrapKey
= secDeriveKeyFromAnswers(answers
, theLocale
);
411 if((wrappedPassword
= encryptString(wrapKey
, iv
, password
)) != NULL
) {
412 retval
= CFDictionaryCreateMutable(kCFAllocatorDefault
, 5, &kCFTypeDictionaryKeyCallBacks
, &kCFTypeDictionaryValueCallBacks
);
413 CFDictionaryAddValue(retval
, kSecRecVersionNumber
, CFNumberCreate(kCFAllocatorDefault
, kCFNumberSInt32Type
, &vers
));
414 CFDictionaryAddValue(retval
, kSecRecQuestions
, questions
);
415 CFDictionaryAddValue(retval
, kSecRecLocale
, theLocaleString
);
416 CFDictionaryAddValue(retval
, kSecRecIV
, b64encode(iv
));
417 CFDictionaryAddValue(retval
, kSecRecWrappedPassword
, wrappedPassword
);
420 if(wrappedPassword
) CFRelease(wrappedPassword
);
423 CFRelease(theLocale
);
424 CFRelease(theLocaleString
);
430 * Function: SecUnwrapRecoveryPasswordWithAnswers
431 * Description: This will unwrap a password contained in a recovery dictionary by using answers
436 SecUnwrapRecoveryPasswordWithAnswers(CFDictionaryRef recref
, CFArrayRef answers
)
438 if(answers
== NULL
|| CFArrayGetCount(answers
) < 3) return NULL
;
440 CFStringRef theLocaleString
= (CFStringRef
) CFDictionaryGetValue(recref
, kSecRecLocale
);
441 CFDataRef tmpIV
= (CFDataRef
) CFDictionaryGetValue(recref
, kSecRecIV
);
442 CFDataRef wrappedPassword
= (CFDataRef
) CFDictionaryGetValue(recref
, kSecRecWrappedPassword
);
444 if(theLocaleString
== NULL
|| tmpIV
== NULL
|| wrappedPassword
== NULL
) {
449 CFLocaleRef theLocale
= CFLocaleCreate(kCFAllocatorDefault
, theLocaleString
);
450 SecKeyRef wrapKey
= secDeriveKeyFromAnswers(answers
, theLocale
);
451 CFRelease(theLocaleString
);
452 CFRelease(theLocale
);
454 CFDataRef iv
= b64decode(tmpIV
);
456 CFStringRef recoveryPassword
= decryptString(wrapKey
, iv
, wrappedPassword
);
459 if(recoveryPassword
!= NULL
) {
460 CFDataRef comphash
= createIVFromPassword(recoveryPassword
);
461 if(!CFEqual(comphash
, iv
)) {
462 secDebug(ASL_LEVEL_ERR
, "Failed reconstitution of password for recovery\n", NULL
);
463 CFRelease(recoveryPassword
);
464 recoveryPassword
= NULL
;
469 return recoveryPassword
;
473 * Function: SecCreateRecoveryPassword
474 * Description: This function will get a random 128 bit number and base32
475 * encode and format that value
479 SecCreateRecoveryPassword(void)
481 CFStringRef result
= NULL
;
482 CFErrorRef error
= NULL
;
483 CFDataRef encodedData
= NULL
;
484 CFDataRef randData
= getRandomBytes(16);
487 // base32FDE is a "private" base32 encoding, it has no 0/O or L/l/1 in it (it uses 8 and 9).
488 SecTransformRef encodeTrans
= SecEncodeTransformCreate(CFSTR("base32FDE"), &error
);
490 SecTransformSetAttribute(encodeTrans
, kSecTransformInputAttributeName
, randData
, &error
);
491 if(error
== NULL
) encodedData
= SecTransformExecute(encodeTrans
, &error
);
492 CFRelease(encodeTrans
);
496 if(encodedData
!= NULL
&& error
== NULL
) {
497 CFStringRef b32string
= CFStringCreateFromExternalRepresentation(kCFAllocatorDefault
, encodedData
, kCFStringEncodingMacRoman
);
498 CFMutableStringRef encodedString
= CFStringCreateMutableCopy(kCFAllocatorDefault
, 64, b32string
);
500 // Add some hyphens to make the generated password easier to use
501 for(i
= 4; i
< 34; i
+= 5) CFStringInsert(encodedString
, i
, CFSTR("-"));
502 // Trim so the last section is 4 characters long
503 CFStringDelete(encodedString
, CFRangeMake(29,CFStringGetLength(encodedString
)-29));
504 result
= CFStringCreateCopy(kCFAllocatorDefault
, encodedString
);
505 CFRelease(encodedString
);
506 CFRelease(b32string
);
507 CFRelease(encodedData
);
509 secDebug(ASL_LEVEL_ERR
, "Failed to base32 encode random data for recovery password\n", NULL
);