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>
42 CFStringRef kSecRecVersionNumber
= CFSTR("SRVersionNumber");
43 CFStringRef kSecRecQuestions
= CFSTR("SRQuestions");
44 CFStringRef kSecRecLocale
= CFSTR("SRLocale");
45 CFStringRef kSecRecIV
= CFSTR("SRiv");
46 CFStringRef kSecRecWrappedPassword
= CFSTR("SRWrappedPassword");
49 static char *std_log_prefix
= "###SecRecovery Function: %s - %s";
50 static const char *std_ident
= "Security.framework";
51 static const char *std_facility
= "InfoSec";
52 static uint32_t std_options
= 0;
54 static aslclient aslhandle
= NULL
;
55 static aslmsg msgptr
= NULL
;
59 void ccdebug_imp(int level
, char *funcname
, char *format
, ...);
61 #define secDebug(lvl,fmt,...) sec_debug_imp(lvl, __PRETTY_FUNCTION__, fmt, __VA_ARGS__)
65 char *ccEnvStdErr
= getenv("CC_STDERR");
67 if(ccEnvStdErr
!= NULL
&& strncmp(ccEnvStdErr
, "yes", 3) == 0) std_options
|= ASL_OPT_STDERR
;
68 aslhandle
= asl_open(std_ident
, std_facility
, std_options
);
70 msgptr
= asl_new(ASL_TYPE_MSG
);
71 asl_set(msgptr
, ASL_KEY_FACILITY
, "com.apple.infosec");
76 sec_debug_imp(int level
, const char *funcname
, char *format
, ...) {
80 if(aslhandle
== NULL
) sec_debug_init();
82 sprintf(fmtbuffer
, std_log_prefix
, funcname
, format
);
83 va_start(argp
, format
);
84 asl_vlog(aslhandle
, msgptr
, level
, fmtbuffer
, argp
);
88 // Read /dev/random for random bytes
91 getRandomBytes(size_t len
)
94 CFDataRef randData
= NULL
;
97 if((buffer
= malloc(len
)) == NULL
) return NULL
;
98 if((fdrand
= open("/dev/random", O_RDONLY
)) == -1) return NULL
;
99 if(read(fdrand
, buffer
, len
) == len
) randData
= CFDataCreate(kCFAllocatorDefault
, (const UInt8
*) buffer
, len
);
106 // This is the normalization routine - subject to change. We need to make sure that whitespace is removed and
107 // that upper/lower case is normalized, etc for all possible languages.
109 static void secNormalize(CFMutableStringRef theString
, CFLocaleRef theLocale
)
113 CFStringFold(theString
, kCFCompareCaseInsensitive
| kCFCompareDiacriticInsensitive
| kCFCompareWidthInsensitive
, theLocale
);
114 CFStringNormalize(theString
, kCFStringNormalizationFormKC
);
115 CFStringTrimWhitespace(theString
);
116 while(CFStringFindCharacterFromSet(theString
, CFCharacterSetGetPredefined(kCFCharacterSetWhitespace
), CFRangeMake(0, CFStringGetLength(theString
)), kCFCompareBackwards
, &theRange
))
117 CFStringDelete(theString
, theRange
);
121 * This will derive a 128 bit (16 byte) key from a set of answers to questions in a CFArray of CFStrings.
122 * it normalizes each answer and concats them into a collector buffer. The resulting string is run through
123 * PBKDF2-HMAC-SHA256 to form a key.
125 * Todo: For version 2 it would be better to randomly generate the salt and make the iteration count flexible.
126 * This would require a different return value because that information would need to be returned up the stack
127 * to the callers. Given the time left in this release (Lion) we're going with set values for this.
130 #define RETURN_KEY_SIZE 16
131 #define MAXANSWERBUFF 4096
132 #define PBKDF_ROUNDS 100000
134 static SecKeyRef
secDeriveKeyFromAnswers(CFArrayRef answers
, CFLocaleRef theLocale
)
136 static const uint8_t salt
[16] = { 0x0A, 0x1F, 0x0A, 0x1F, 0x0A, 0x1F, 0x0A, 0x1F, 0x0A, 0x1F, 0x0A, 0x1F, 0x0A, 0x1F, 0x0A, 0x1F };
137 static const int saltLen
= sizeof(salt
);
139 SecKeyRef theKey
= NULL
;
140 uint8_t rawKeyData
[RETURN_KEY_SIZE
];
142 CFIndex encodedAnswers
= 0;
143 CFIndex numAnswers
= CFArrayGetCount(answers
);
144 const size_t concatenatedAnswersSize
= MAXANSWERBUFF
* numAnswers
;
146 char *concatenatedAnswers
= (char *)malloc(concatenatedAnswersSize
);
147 if (concatenatedAnswers
== NULL
) {
151 concatenatedAnswers
[0] = 0; // NUL terminate
154 for (i
= 0; i
< numAnswers
; i
++) {
155 CFMutableStringRef answer
= CFStringCreateMutableCopy(kCFAllocatorDefault
, 0, CFArrayGetValueAtIndex(answers
, i
));
157 secNormalize(answer
, theLocale
);
159 CFIndex theAnswerLen
= CFStringGetLength(answer
);
160 CFIndex theAnswerSize
= CFStringGetMaximumSizeForEncoding(theAnswerLen
, kCFStringEncodingUTF8
);
161 char *theAnswer
= (char *)malloc(theAnswerSize
+ 1); // add space for NUL byte
163 if (theAnswerLen
== CFStringGetBytes(answer
, CFRangeMake(0, CFStringGetLength(answer
)), kCFStringEncodingUTF8
, '?', FALSE
, (UInt8
*)theAnswer
, theAnswerSize
, &theAnswerSize
)) {
164 theAnswer
[theAnswerSize
] = 0; // NUL terminate
165 if (strlcat(concatenatedAnswers
, theAnswer
, concatenatedAnswersSize
) < concatenatedAnswersSize
) {
169 bzero(theAnswer
, theAnswerSize
);
176 // one or more of the answers failed to encode
177 if (encodedAnswers
!= numAnswers
) {
178 free(concatenatedAnswers
);
182 if (CCKeyDerivationPBKDF(kCCPBKDF2
, concatenatedAnswers
, strlen(concatenatedAnswers
), salt
, saltLen
, kCCPRFHmacAlgSHA256
, PBKDF_ROUNDS
, rawKeyData
, RETURN_KEY_SIZE
)) {
183 free(concatenatedAnswers
);
187 CFDataRef keyData
= CFDataCreate(kCFAllocatorDefault
, rawKeyData
, RETURN_KEY_SIZE
);
189 CFMutableDictionaryRef params
= CFDictionaryCreateMutable(kCFAllocatorDefault
, 1, &kCFTypeDictionaryKeyCallBacks
, &kCFTypeDictionaryValueCallBacks
);
191 CFErrorRef error
= NULL
;
192 CFDictionaryAddValue(params
, kSecAttrKeyType
, kSecAttrKeyTypeAES
);
193 theKey
= SecKeyCreateFromData(params
, keyData
, &error
);
202 bzero(rawKeyData
, RETURN_KEY_SIZE
);
203 bzero(concatenatedAnswers
, concatenatedAnswersSize
);
204 free(concatenatedAnswers
);
209 // Single shot CFString processing routines for digests/encoding/encrypt/decrypt
212 digestString(CFStringRef str
)
214 CFDataRef retval
= NULL
;
215 CFErrorRef error
= NULL
;
217 CFDataRef inputString
= CFStringCreateExternalRepresentation(kCFAllocatorDefault
, str
, kCFStringEncodingUTF8
, 0xff);
219 SecTransformRef digestTrans
= SecDigestTransformCreate(kSecDigestSHA2
, 256, &error
);
221 SecTransformSetAttribute(digestTrans
, kSecTransformInputAttributeName
, inputString
, &error
);
223 retval
= SecTransformExecute(digestTrans
, &error
);
225 secDebug(ASL_LEVEL_ERR
, "Couldn't create digest %s\n", CFStringGetCStringPtr(CFErrorCopyFailureReason(error
), kCFStringEncodingUTF8
));
228 CFRelease(digestTrans
);
230 CFRelease(inputString
);
234 static CFDataRef CF_RETURNS_RETAINED
235 b64encode(CFDataRef input
)
237 CFDataRef retval
= NULL
;
238 CFErrorRef error
= NULL
;
239 SecTransformRef encodeTrans
= SecEncodeTransformCreate(kSecBase64Encoding
, &error
);
240 if(error
== NULL
) SecTransformSetAttribute(encodeTrans
, kSecTransformInputAttributeName
, input
, &error
);
241 if(error
== NULL
) retval
= SecTransformExecute(encodeTrans
, &error
);
242 if(encodeTrans
) CFRelease(encodeTrans
);
246 static CFDataRef CF_RETURNS_RETAINED
247 b64decode(CFDataRef input
)
249 CFDataRef retval
= NULL
;
250 CFErrorRef error
= NULL
;
251 SecTransformRef decodeTrans
= SecDecodeTransformCreate(kSecBase64Encoding
, &error
);
252 if(error
== NULL
) SecTransformSetAttribute(decodeTrans
, kSecTransformInputAttributeName
, input
, &error
);
253 if(error
== NULL
) retval
= SecTransformExecute(decodeTrans
, &error
);
254 if(decodeTrans
) CFRelease(decodeTrans
);
259 encryptString(SecKeyRef wrapKey
, CFDataRef iv
, CFStringRef str
)
261 CFDataRef retval
= NULL
;
262 CFErrorRef error
= NULL
;
263 CFDataRef inputString
= CFStringCreateExternalRepresentation(kCFAllocatorDefault
, str
, kCFStringEncodingMacRoman
, 0xff);
265 SecTransformRef encryptTrans
= SecEncryptTransformCreate(wrapKey
, &error
);
267 SecTransformRef group
= SecTransformCreateGroupTransform();
269 SecTransformSetAttribute(encryptTrans
, kSecEncryptionMode
, kSecModeCBCKey
, &error
);
270 if(error
== NULL
) SecTransformSetAttribute(encryptTrans
, kSecPaddingKey
, kSecPaddingPKCS7Key
, &error
);
271 if(error
== NULL
) SecTransformSetAttribute(encryptTrans
, kSecTransformInputAttributeName
, inputString
, &error
);
272 if(error
== NULL
) SecTransformSetAttribute(encryptTrans
, kSecIVKey
, iv
, &error
);
273 SecTransformRef encodeTrans
= SecEncodeTransformCreate(kSecBase64Encoding
, &error
);
274 SecTransformConnectTransforms(encryptTrans
, kSecTransformOutputAttributeName
, encodeTrans
, kSecTransformInputAttributeName
, group
, &error
);
275 CFRelease(encodeTrans
);
276 CFRelease(encryptTrans
);
277 if(error
== NULL
) retval
= SecTransformExecute(group
, &error
);
278 if(error
!= NULL
) secDebug(ASL_LEVEL_ERR
, "Failed to encrypt recovery password\n", NULL
);
285 static CFStringRef CF_RETURNS_RETAINED
286 decryptString(SecKeyRef wrapKey
, CFDataRef iv
, CFDataRef wrappedPassword
)
288 CFStringRef retval
= NULL
;
289 CFDataRef retData
= NULL
;
290 CFErrorRef error
= NULL
;
292 SecTransformRef decryptTrans
= SecDecryptTransformCreate(wrapKey
, &error
);
294 SecTransformRef group
= SecTransformCreateGroupTransform();
296 SecTransformRef decodeTrans
= SecDecodeTransformCreate(kSecBase64Encoding
, &error
);
297 if(error
== NULL
) SecTransformSetAttribute(decodeTrans
, kSecTransformInputAttributeName
, wrappedPassword
, &error
);
299 if(error
== NULL
) SecTransformSetAttribute(decryptTrans
, kSecEncryptionMode
, kSecModeCBCKey
, &error
);
300 if(error
== NULL
) SecTransformSetAttribute(decryptTrans
, kSecPaddingKey
, kSecPaddingPKCS7Key
, &error
);
301 if(error
== NULL
) SecTransformSetAttribute(decryptTrans
, kSecIVKey
, iv
, &error
);
302 SecTransformConnectTransforms(decodeTrans
, kSecTransformOutputAttributeName
, decryptTrans
, kSecTransformInputAttributeName
, group
, &error
);
303 CFRelease(decodeTrans
);
304 CFRelease(decryptTrans
);
305 if(error
== NULL
) retData
= SecTransformExecute(group
, &error
);
307 if(error
== NULL
) retval
= CFStringCreateFromExternalRepresentation(kCFAllocatorDefault
, retData
, kCFStringEncodingMacRoman
);
308 else secDebug(ASL_LEVEL_ERR
, "Failed to decrypt recovery password\n", NULL
);
311 CFReleaseNull(decryptTrans
);
315 // IV for the recovery ref is currently the leftmost 16 bytes of the digest of the recovery password.
316 #define IVBYTECOUNT 16
319 createIVFromPassword(CFStringRef password
)
321 CFDataRef hashedPassword
, retval
;
323 if((hashedPassword
= digestString(password
)) == NULL
) return NULL
;
324 iv
= CFDataCreateMutableCopy(kCFAllocatorDefault
, CFDataGetLength(hashedPassword
)+1, hashedPassword
);
325 CFDataDeleteBytes(iv
, CFRangeMake(IVBYTECOUNT
, CFDataGetLength(iv
)-IVBYTECOUNT
));
326 retval
= CFDataCreateCopy(kCFAllocatorDefault
, iv
);
327 CFRelease(hashedPassword
);
338 * Function: SecWrapRecoveryPasswordWithAnswers
339 * Description: This will wrap a password by using answers to generate a key. The resulting
340 * wrapped password and the questions used to get the answers are saved in a
341 * recovery dictionary.
345 SecWrapRecoveryPasswordWithAnswers(CFStringRef password
, CFArrayRef questions
, CFArrayRef answers
)
349 CFDataRef wrappedPassword
;
350 CFMutableDictionaryRef retval
= NULL
;
351 CFLocaleRef theLocale
= CFLocaleCopyCurrent();
352 CFStringRef theLocaleString
= CFLocaleGetIdentifier(theLocale
);
356 if (!password
|| !questions
|| !answers
)
359 limit
= CFArrayGetCount(answers
);
360 if (limit
!= CFArrayGetCount(questions
))
361 return NULL
; // Error
363 for (ix
=0; ix
<limit
; ix
++)
365 chkval
= CFArrayGetValueAtIndex(answers
, ix
);
366 if (!chkval
|| CFGetTypeID(chkval
)!=CFStringGetTypeID() || CFEqual((CFStringRef
)chkval
, CFSTR("")))
368 chkval
= CFArrayGetValueAtIndex(questions
, ix
);
369 if (!chkval
|| CFGetTypeID(chkval
)!=CFStringGetTypeID() || CFEqual((CFStringRef
)chkval
, CFSTR("")))
373 iv
= createIVFromPassword(password
);
375 SecKeyRef wrapKey
= secDeriveKeyFromAnswers(answers
, theLocale
);
377 if((wrappedPassword
= encryptString(wrapKey
, iv
, password
)) != NULL
) {
378 retval
= CFDictionaryCreateMutable(kCFAllocatorDefault
, 5, &kCFTypeDictionaryKeyCallBacks
, &kCFTypeDictionaryValueCallBacks
);
379 CFDictionaryAddValue(retval
, kSecRecVersionNumber
, CFNumberCreate(kCFAllocatorDefault
, kCFNumberSInt32Type
, &vers
));
380 CFDictionaryAddValue(retval
, kSecRecQuestions
, questions
);
381 CFDictionaryAddValue(retval
, kSecRecLocale
, theLocaleString
);
382 CFDictionaryAddValue(retval
, kSecRecIV
, b64encode(iv
));
383 CFDictionaryAddValue(retval
, kSecRecWrappedPassword
, wrappedPassword
);
386 if(wrappedPassword
) CFRelease(wrappedPassword
);
389 CFRelease(theLocale
);
390 CFRelease(theLocaleString
);
396 * Function: SecUnwrapRecoveryPasswordWithAnswers
397 * Description: This will unwrap a password contained in a recovery dictionary by using answers
402 SecUnwrapRecoveryPasswordWithAnswers(CFDictionaryRef recref
, CFArrayRef answers
)
404 if(answers
== NULL
|| CFArrayGetCount(answers
) < 3) return NULL
;
406 CFStringRef theLocaleString
= (CFStringRef
) CFDictionaryGetValue(recref
, kSecRecLocale
);
407 CFDataRef tmpIV
= (CFDataRef
) CFDictionaryGetValue(recref
, kSecRecIV
);
408 CFDataRef wrappedPassword
= (CFDataRef
) CFDictionaryGetValue(recref
, kSecRecWrappedPassword
);
410 if(theLocaleString
== NULL
|| tmpIV
== NULL
|| wrappedPassword
== NULL
) {
415 CFLocaleRef theLocale
= CFLocaleCreate(kCFAllocatorDefault
, theLocaleString
);
416 SecKeyRef wrapKey
= secDeriveKeyFromAnswers(answers
, theLocale
);
417 CFRelease(theLocaleString
);
418 CFRelease(theLocale
);
420 CFDataRef iv
= b64decode(tmpIV
);
422 CFStringRef recoveryPassword
= decryptString(wrapKey
, iv
, wrappedPassword
);
425 if(recoveryPassword
!= NULL
) {
426 CFDataRef comphash
= createIVFromPassword(recoveryPassword
);
427 if(!CFEqual(comphash
, iv
)) {
428 secDebug(ASL_LEVEL_ERR
, "Failed reconstitution of password for recovery\n", NULL
);
429 CFRelease(recoveryPassword
);
430 recoveryPassword
= NULL
;
435 return recoveryPassword
;
439 * Function: SecCreateRecoveryPassword
440 * Description: This function will get a random 128 bit number and base32
441 * encode and format that value
445 SecCreateRecoveryPassword(void)
447 CFStringRef result
= NULL
;
448 CFErrorRef error
= NULL
;
449 CFDataRef encodedData
= NULL
;
450 CFDataRef randData
= getRandomBytes(16);
453 // base32FDE is a "private" base32 encoding, it has no 0/O or L/l/1 in it (it uses 8 and 9).
454 SecTransformRef encodeTrans
= SecEncodeTransformCreate(CFSTR("base32FDE"), &error
);
456 SecTransformSetAttribute(encodeTrans
, kSecTransformInputAttributeName
, randData
, &error
);
457 if(error
== NULL
) encodedData
= SecTransformExecute(encodeTrans
, &error
);
458 CFRelease(encodeTrans
);
462 if(encodedData
!= NULL
&& error
== NULL
) {
463 CFStringRef b32string
= CFStringCreateFromExternalRepresentation(kCFAllocatorDefault
, encodedData
, kCFStringEncodingMacRoman
);
464 CFMutableStringRef encodedString
= CFStringCreateMutableCopy(kCFAllocatorDefault
, 64, b32string
);
466 // Add some hyphens to make the generated password easier to use
467 for(i
= 4; i
< 34; i
+= 5) CFStringInsert(encodedString
, i
, CFSTR("-"));
468 // Trim so the last section is 4 characters long
469 CFStringDelete(encodedString
, CFRangeMake(29,CFStringGetLength(encodedString
)-29));
470 result
= CFStringCreateCopy(kCFAllocatorDefault
, encodedString
);
471 CFRelease(encodedString
);
472 CFRelease(b32string
);
473 CFRelease(encodedData
);
475 secDebug(ASL_LEVEL_ERR
, "Failed to base32 encode random data for recovery password\n", NULL
);