2 * Copyright (c) 2010 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
132 static uint8_t salt
[16] = { 0x0A, 0x1F, 0x0A, 0x1F, 0x0A, 0x1F, 0x0A, 0x1F, 0x0A, 0x1F, 0x0A, 0x1F, 0x0A, 0x1F, 0x0A, 0x1F };
133 static int saltLen
= sizeof(salt
);
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
);
236 b64encode(CFDataRef input
)
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
);
248 b64decode(CFDataRef input
)
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);
266 SecTransformRef encryptTrans
= SecEncryptTransformCreate(wrapKey
, &error
);
268 SecTransformRef group
= SecTransformCreateGroupTransform();
270 SecTransformSetAttribute(encryptTrans
, kSecEncryptionMode
, kSecModeCBCKey
, &error
);
271 if(error
== NULL
) SecTransformSetAttribute(encryptTrans
, kSecPaddingKey
, kSecPaddingPKCS7Key
, &error
);
272 if(error
== NULL
) SecTransformSetAttribute(encryptTrans
, kSecTransformInputAttributeName
, inputString
, &error
);
273 if(error
== NULL
) SecTransformSetAttribute(encryptTrans
, kSecIVKey
, iv
, &error
);
274 SecTransformRef encodeTrans
= SecEncodeTransformCreate(kSecBase64Encoding
, &error
);
275 SecTransformConnectTransforms(encryptTrans
, kSecTransformOutputAttributeName
, encodeTrans
, kSecTransformInputAttributeName
, group
, &error
);
276 CFRelease(encodeTrans
);
277 CFRelease(encryptTrans
);
278 if(error
== NULL
) retval
= SecTransformExecute(group
, &error
);
279 if(error
!= NULL
) secDebug(ASL_LEVEL_ERR
, "Failed to encrypt recovery password\n", NULL
);
287 decryptString(SecKeyRef wrapKey
, CFDataRef iv
, CFDataRef wrappedPassword
)
289 CFStringRef retval
= NULL
;
290 CFDataRef retData
= NULL
;
291 CFErrorRef error
= NULL
;
293 SecTransformRef decryptTrans
= SecDecryptTransformCreate(wrapKey
, &error
);
295 SecTransformRef group
= SecTransformCreateGroupTransform();
297 SecTransformRef decodeTrans
= SecDecodeTransformCreate(kSecBase64Encoding
, &error
);
298 if(error
== NULL
) SecTransformSetAttribute(decodeTrans
, kSecTransformInputAttributeName
, wrappedPassword
, &error
);
300 if(error
== NULL
) SecTransformSetAttribute(decryptTrans
, kSecEncryptionMode
, kSecModeCBCKey
, &error
);
301 if(error
== NULL
) SecTransformSetAttribute(decryptTrans
, kSecPaddingKey
, kSecPaddingPKCS7Key
, &error
);
302 if(error
== NULL
) SecTransformSetAttribute(decryptTrans
, kSecIVKey
, iv
, &error
);
303 SecTransformConnectTransforms(decodeTrans
, kSecTransformOutputAttributeName
, decryptTrans
, kSecTransformInputAttributeName
, group
, &error
);
304 CFRelease(decodeTrans
);
305 CFRelease(decryptTrans
);
306 if(error
== NULL
) retData
= SecTransformExecute(group
, &error
);
308 if(error
== NULL
) retval
= CFStringCreateFromExternalRepresentation(kCFAllocatorDefault
, retData
, kCFStringEncodingMacRoman
);
309 else secDebug(ASL_LEVEL_ERR
, "Failed to decrypt recovery password\n", NULL
);
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()
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
);