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>
41 CFStringRef kSecRecVersionNumber
= CFSTR("SRVersionNumber");
42 CFStringRef kSecRecQuestions
= CFSTR("SRQuestions");
43 CFStringRef kSecRecLocale
= CFSTR("SRLocale");
44 CFStringRef kSecRecIV
= CFSTR("SRiv");
45 CFStringRef kSecRecWrappedPassword
= CFSTR("SRWrappedPassword");
48 static char *std_log_prefix
= "###SecRecovery Function: %s - %s";
49 static const char *std_ident
= "Security.framework";
50 static const char *std_facility
= "InfoSec";
51 static uint32_t std_options
= 0;
53 static aslclient aslhandle
= NULL
;
54 static aslmsg msgptr
= NULL
;
58 void ccdebug_imp(int level
, char *funcname
, char *format
, ...);
60 #define secDebug(lvl,fmt,...) sec_debug_imp(lvl, __PRETTY_FUNCTION__, fmt, __VA_ARGS__)
64 char *ccEnvStdErr
= getenv("CC_STDERR");
66 if(ccEnvStdErr
!= NULL
&& strncmp(ccEnvStdErr
, "yes", 3) == 0) std_options
|= ASL_OPT_STDERR
;
67 aslhandle
= asl_open(std_ident
, std_facility
, std_options
);
69 msgptr
= asl_new(ASL_TYPE_MSG
);
70 asl_set(msgptr
, ASL_KEY_FACILITY
, "com.apple.infosec");
75 sec_debug_imp(int level
, const char *funcname
, char *format
, ...) {
79 if(aslhandle
== NULL
) sec_debug_init();
81 sprintf(fmtbuffer
, std_log_prefix
, funcname
, format
);
82 va_start(argp
, format
);
83 asl_vlog(aslhandle
, msgptr
, level
, fmtbuffer
, argp
);
87 // Read /dev/random for random bytes
90 getRandomBytes(size_t len
)
93 CFDataRef randData
= NULL
;
96 if((buffer
= malloc(len
)) == NULL
) return NULL
;
97 if((fdrand
= open("/dev/random", O_RDONLY
)) == -1) return NULL
;
98 if(read(fdrand
, buffer
, len
) == len
) randData
= CFDataCreate(kCFAllocatorDefault
, (const UInt8
*) buffer
, len
);
105 // This is the normalization routine - subject to change. We need to make sure that whitespace is removed and
106 // that upper/lower case is normalized, etc for all possible languages.
108 static void secNormalize(CFMutableStringRef theString
, CFLocaleRef theLocale
)
112 CFStringFold(theString
, kCFCompareCaseInsensitive
| kCFCompareDiacriticInsensitive
| kCFCompareWidthInsensitive
, theLocale
);
113 CFStringNormalize(theString
, kCFStringNormalizationFormKC
);
114 CFStringTrimWhitespace(theString
);
115 while(CFStringFindCharacterFromSet(theString
, CFCharacterSetGetPredefined(kCFCharacterSetWhitespace
), CFRangeMake(0, CFStringGetLength(theString
)), kCFCompareBackwards
, &theRange
))
116 CFStringDelete(theString
, theRange
);
120 * This will derive a 128 bit (16 byte) key from a set of answers to questions in a CFArray of CFStrings.
121 * it normalizes each answer and concats them into a collector buffer. The resulting string is run through
122 * PBKDF2-HMAC-SHA256 to form a key.
124 * Todo: For version 2 it would be better to randomly generate the salt and make the iteration count flexible.
125 * This would require a different return value because that information would need to be returned up the stack
126 * to the callers. Given the time left in this release (Lion) we're going with set values for this.
129 #define RETURN_KEY_SIZE 16
130 #define MAXANSWERBUFF 4096
131 #define PBKDF_ROUNDS 100000
133 static SecKeyRef
secDeriveKeyFromAnswers(CFArrayRef answers
, CFLocaleRef theLocale
)
135 static const uint8_t salt
[16] = { 0x0A, 0x1F, 0x0A, 0x1F, 0x0A, 0x1F, 0x0A, 0x1F, 0x0A, 0x1F, 0x0A, 0x1F, 0x0A, 0x1F, 0x0A, 0x1F };
136 static const int saltLen
= sizeof(salt
);
138 SecKeyRef theKey
= NULL
;
139 uint8_t rawKeyData
[RETURN_KEY_SIZE
];
141 CFIndex encodedAnswers
= 0;
142 CFIndex numAnswers
= CFArrayGetCount(answers
);
143 const size_t concatenatedAnswersSize
= MAXANSWERBUFF
* numAnswers
;
145 char *concatenatedAnswers
= (char *)malloc(concatenatedAnswersSize
);
146 if (concatenatedAnswers
== NULL
) {
150 concatenatedAnswers
[0] = 0; // NUL terminate
153 for (i
= 0; i
< numAnswers
; i
++) {
154 CFMutableStringRef answer
= CFStringCreateMutableCopy(kCFAllocatorDefault
, 0, CFArrayGetValueAtIndex(answers
, i
));
156 secNormalize(answer
, theLocale
);
158 CFIndex theAnswerLen
= CFStringGetLength(answer
);
159 CFIndex theAnswerSize
= CFStringGetMaximumSizeForEncoding(theAnswerLen
, kCFStringEncodingUTF8
);
160 char *theAnswer
= (char *)malloc(theAnswerSize
+ 1); // add space for NUL byte
162 if (theAnswerLen
== CFStringGetBytes(answer
, CFRangeMake(0, CFStringGetLength(answer
)), kCFStringEncodingUTF8
, '?', FALSE
, (UInt8
*)theAnswer
, theAnswerSize
, &theAnswerSize
)) {
163 theAnswer
[theAnswerSize
] = 0; // NUL terminate
164 if (strlcat(concatenatedAnswers
, theAnswer
, concatenatedAnswersSize
) < concatenatedAnswersSize
) {
168 bzero(theAnswer
, theAnswerSize
);
175 // one or more of the answers failed to encode
176 if (encodedAnswers
!= numAnswers
) {
177 free(concatenatedAnswers
);
181 if (CCKeyDerivationPBKDF(kCCPBKDF2
, concatenatedAnswers
, strlen(concatenatedAnswers
), salt
, saltLen
, kCCPRFHmacAlgSHA256
, PBKDF_ROUNDS
, rawKeyData
, RETURN_KEY_SIZE
)) {
182 free(concatenatedAnswers
);
186 CFDataRef keyData
= CFDataCreate(kCFAllocatorDefault
, rawKeyData
, RETURN_KEY_SIZE
);
188 CFMutableDictionaryRef params
= CFDictionaryCreateMutable(kCFAllocatorDefault
, 1, &kCFTypeDictionaryKeyCallBacks
, &kCFTypeDictionaryValueCallBacks
);
190 CFErrorRef error
= NULL
;
191 CFDictionaryAddValue(params
, kSecAttrKeyType
, kSecAttrKeyTypeAES
);
192 theKey
= SecKeyCreateFromData(params
, keyData
, &error
);
201 bzero(rawKeyData
, RETURN_KEY_SIZE
);
202 bzero(concatenatedAnswers
, concatenatedAnswersSize
);
203 free(concatenatedAnswers
);
208 // Single shot CFString processing routines for digests/encoding/encrypt/decrypt
211 digestString(CFStringRef str
)
213 CFDataRef retval
= NULL
;
214 CFErrorRef error
= NULL
;
216 CFDataRef inputString
= CFStringCreateExternalRepresentation(kCFAllocatorDefault
, str
, kCFStringEncodingUTF8
, 0xff);
218 SecTransformRef digestTrans
= SecDigestTransformCreate(kSecDigestSHA2
, 256, &error
);
220 SecTransformSetAttribute(digestTrans
, kSecTransformInputAttributeName
, inputString
, &error
);
222 retval
= SecTransformExecute(digestTrans
, &error
);
224 secDebug(ASL_LEVEL_ERR
, "Couldn't create digest %s\n", CFStringGetCStringPtr(CFErrorCopyFailureReason(error
), kCFStringEncodingUTF8
));
227 CFRelease(digestTrans
);
229 CFRelease(inputString
);
234 b64encode(CFDataRef input
)
236 CFDataRef retval
= NULL
;
237 CFErrorRef error
= NULL
;
238 SecTransformRef encodeTrans
= SecEncodeTransformCreate(kSecBase64Encoding
, &error
);
239 if(error
== NULL
) SecTransformSetAttribute(encodeTrans
, kSecTransformInputAttributeName
, input
, &error
);
240 if(error
== NULL
) retval
= SecTransformExecute(encodeTrans
, &error
);
241 if(encodeTrans
) CFRelease(encodeTrans
);
246 b64decode(CFDataRef input
)
248 CFDataRef retval
= NULL
;
249 CFErrorRef error
= NULL
;
250 SecTransformRef decodeTrans
= SecDecodeTransformCreate(kSecBase64Encoding
, &error
);
251 if(error
== NULL
) SecTransformSetAttribute(decodeTrans
, kSecTransformInputAttributeName
, input
, &error
);
252 if(error
== NULL
) retval
= SecTransformExecute(decodeTrans
, &error
);
253 if(decodeTrans
) CFRelease(decodeTrans
);
258 encryptString(SecKeyRef wrapKey
, CFDataRef iv
, CFStringRef str
)
260 CFDataRef retval
= NULL
;
261 CFErrorRef error
= NULL
;
262 CFDataRef inputString
= CFStringCreateExternalRepresentation(kCFAllocatorDefault
, str
, kCFStringEncodingMacRoman
, 0xff);
264 SecTransformRef encryptTrans
= SecEncryptTransformCreate(wrapKey
, &error
);
266 SecTransformRef group
= SecTransformCreateGroupTransform();
268 SecTransformSetAttribute(encryptTrans
, kSecEncryptionMode
, kSecModeCBCKey
, &error
);
269 if(error
== NULL
) SecTransformSetAttribute(encryptTrans
, kSecPaddingKey
, kSecPaddingPKCS7Key
, &error
);
270 if(error
== NULL
) SecTransformSetAttribute(encryptTrans
, kSecTransformInputAttributeName
, inputString
, &error
);
271 if(error
== NULL
) SecTransformSetAttribute(encryptTrans
, kSecIVKey
, iv
, &error
);
272 SecTransformRef encodeTrans
= SecEncodeTransformCreate(kSecBase64Encoding
, &error
);
273 SecTransformConnectTransforms(encryptTrans
, kSecTransformOutputAttributeName
, encodeTrans
, kSecTransformInputAttributeName
, group
, &error
);
274 CFRelease(encodeTrans
);
275 CFRelease(encryptTrans
);
276 if(error
== NULL
) retval
= SecTransformExecute(group
, &error
);
277 if(error
!= NULL
) secDebug(ASL_LEVEL_ERR
, "Failed to encrypt recovery password\n", NULL
);
285 decryptString(SecKeyRef wrapKey
, CFDataRef iv
, CFDataRef wrappedPassword
)
287 CFStringRef retval
= NULL
;
288 CFDataRef retData
= NULL
;
289 CFErrorRef error
= NULL
;
291 SecTransformRef decryptTrans
= SecDecryptTransformCreate(wrapKey
, &error
);
293 SecTransformRef group
= SecTransformCreateGroupTransform();
295 SecTransformRef decodeTrans
= SecDecodeTransformCreate(kSecBase64Encoding
, &error
);
296 if(error
== NULL
) SecTransformSetAttribute(decodeTrans
, kSecTransformInputAttributeName
, wrappedPassword
, &error
);
298 if(error
== NULL
) SecTransformSetAttribute(decryptTrans
, kSecEncryptionMode
, kSecModeCBCKey
, &error
);
299 if(error
== NULL
) SecTransformSetAttribute(decryptTrans
, kSecPaddingKey
, kSecPaddingPKCS7Key
, &error
);
300 if(error
== NULL
) SecTransformSetAttribute(decryptTrans
, kSecIVKey
, iv
, &error
);
301 SecTransformConnectTransforms(decodeTrans
, kSecTransformOutputAttributeName
, decryptTrans
, kSecTransformInputAttributeName
, group
, &error
);
302 CFRelease(decodeTrans
);
303 CFRelease(decryptTrans
);
304 if(error
== NULL
) retData
= SecTransformExecute(group
, &error
);
306 if(error
== NULL
) retval
= CFStringCreateFromExternalRepresentation(kCFAllocatorDefault
, retData
, kCFStringEncodingMacRoman
);
307 else secDebug(ASL_LEVEL_ERR
, "Failed to decrypt recovery password\n", NULL
);
313 // IV for the recovery ref is currently the leftmost 16 bytes of the digest of the recovery password.
314 #define IVBYTECOUNT 16
317 createIVFromPassword(CFStringRef password
)
319 CFDataRef hashedPassword
, retval
;
321 if((hashedPassword
= digestString(password
)) == NULL
) return NULL
;
322 iv
= CFDataCreateMutableCopy(kCFAllocatorDefault
, CFDataGetLength(hashedPassword
)+1, hashedPassword
);
323 CFDataDeleteBytes(iv
, CFRangeMake(IVBYTECOUNT
, CFDataGetLength(iv
)-IVBYTECOUNT
));
324 retval
= CFDataCreateCopy(kCFAllocatorDefault
, iv
);
325 CFRelease(hashedPassword
);
336 * Function: SecWrapRecoveryPasswordWithAnswers
337 * Description: This will wrap a password by using answers to generate a key. The resulting
338 * wrapped password and the questions used to get the answers are saved in a
339 * recovery dictionary.
343 SecWrapRecoveryPasswordWithAnswers(CFStringRef password
, CFArrayRef questions
, CFArrayRef answers
)
347 CFDataRef wrappedPassword
;
348 CFMutableDictionaryRef retval
= NULL
;
349 CFLocaleRef theLocale
= CFLocaleCopyCurrent();
350 CFStringRef theLocaleString
= CFLocaleGetIdentifier(theLocale
);
354 if (!password
|| !questions
|| !answers
)
357 limit
= CFArrayGetCount(answers
);
358 if (limit
!= CFArrayGetCount(questions
))
359 return NULL
; // Error
361 for (ix
=0; ix
<limit
; ix
++)
363 chkval
= CFArrayGetValueAtIndex(answers
, ix
);
364 if (!chkval
|| CFGetTypeID(chkval
)!=CFStringGetTypeID() || CFEqual((CFStringRef
)chkval
, CFSTR("")))
366 chkval
= CFArrayGetValueAtIndex(questions
, ix
);
367 if (!chkval
|| CFGetTypeID(chkval
)!=CFStringGetTypeID() || CFEqual((CFStringRef
)chkval
, CFSTR("")))
371 iv
= createIVFromPassword(password
);
373 SecKeyRef wrapKey
= secDeriveKeyFromAnswers(answers
, theLocale
);
375 if((wrappedPassword
= encryptString(wrapKey
, iv
, password
)) != NULL
) {
376 retval
= CFDictionaryCreateMutable(kCFAllocatorDefault
, 5, &kCFTypeDictionaryKeyCallBacks
, &kCFTypeDictionaryValueCallBacks
);
377 CFDictionaryAddValue(retval
, kSecRecVersionNumber
, CFNumberCreate(kCFAllocatorDefault
, kCFNumberSInt32Type
, &vers
));
378 CFDictionaryAddValue(retval
, kSecRecQuestions
, questions
);
379 CFDictionaryAddValue(retval
, kSecRecLocale
, theLocaleString
);
380 CFDictionaryAddValue(retval
, kSecRecIV
, b64encode(iv
));
381 CFDictionaryAddValue(retval
, kSecRecWrappedPassword
, wrappedPassword
);
384 if(wrappedPassword
) CFRelease(wrappedPassword
);
387 CFRelease(theLocale
);
388 CFRelease(theLocaleString
);
394 * Function: SecUnwrapRecoveryPasswordWithAnswers
395 * Description: This will unwrap a password contained in a recovery dictionary by using answers
400 SecUnwrapRecoveryPasswordWithAnswers(CFDictionaryRef recref
, CFArrayRef answers
)
402 if(answers
== NULL
|| CFArrayGetCount(answers
) < 3) return NULL
;
404 CFStringRef theLocaleString
= (CFStringRef
) CFDictionaryGetValue(recref
, kSecRecLocale
);
405 CFDataRef tmpIV
= (CFDataRef
) CFDictionaryGetValue(recref
, kSecRecIV
);
406 CFDataRef wrappedPassword
= (CFDataRef
) CFDictionaryGetValue(recref
, kSecRecWrappedPassword
);
408 if(theLocaleString
== NULL
|| tmpIV
== NULL
|| wrappedPassword
== NULL
) {
413 CFLocaleRef theLocale
= CFLocaleCreate(kCFAllocatorDefault
, theLocaleString
);
414 SecKeyRef wrapKey
= secDeriveKeyFromAnswers(answers
, theLocale
);
415 CFRelease(theLocaleString
);
416 CFRelease(theLocale
);
418 CFDataRef iv
= b64decode(tmpIV
);
420 CFStringRef recoveryPassword
= decryptString(wrapKey
, iv
, wrappedPassword
);
423 if(recoveryPassword
!= NULL
) {
424 CFDataRef comphash
= createIVFromPassword(recoveryPassword
);
425 if(!CFEqual(comphash
, iv
)) {
426 secDebug(ASL_LEVEL_ERR
, "Failed reconstitution of password for recovery\n", NULL
);
427 CFRelease(recoveryPassword
);
428 recoveryPassword
= NULL
;
433 return recoveryPassword
;
437 * Function: SecCreateRecoveryPassword
438 * Description: This function will get a random 128 bit number and base32
439 * encode and format that value
443 SecCreateRecoveryPassword(void)
445 CFStringRef result
= NULL
;
446 CFErrorRef error
= NULL
;
447 CFDataRef encodedData
= NULL
;
448 CFDataRef randData
= getRandomBytes(16);
451 // base32FDE is a "private" base32 encoding, it has no 0/O or L/l/1 in it (it uses 8 and 9).
452 SecTransformRef encodeTrans
= SecEncodeTransformCreate(CFSTR("base32FDE"), &error
);
454 SecTransformSetAttribute(encodeTrans
, kSecTransformInputAttributeName
, randData
, &error
);
455 if(error
== NULL
) encodedData
= SecTransformExecute(encodeTrans
, &error
);
456 CFRelease(encodeTrans
);
460 if(encodedData
!= NULL
&& error
== NULL
) {
461 CFStringRef b32string
= CFStringCreateFromExternalRepresentation(kCFAllocatorDefault
, encodedData
, kCFStringEncodingMacRoman
);
462 CFMutableStringRef encodedString
= CFStringCreateMutableCopy(kCFAllocatorDefault
, 64, b32string
);
464 // Add some hyphens to make the generated password easier to use
465 for(i
= 4; i
< 34; i
+= 5) CFStringInsert(encodedString
, i
, CFSTR("-"));
466 // Trim so the last section is 4 characters long
467 CFStringDelete(encodedString
, CFRangeMake(29,CFStringGetLength(encodedString
)-29));
468 result
= CFStringCreateCopy(kCFAllocatorDefault
, encodedString
);
469 CFRelease(encodedString
);
470 CFRelease(b32string
);
471 CFRelease(encodedData
);
473 secDebug(ASL_LEVEL_ERR
, "Failed to base32 encode random data for recovery password\n", NULL
);