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 <Security/SecRandom.h>
33 #include <CommonCrypto/CommonKeyDerivation.h>
34 #include <CommonCrypto/CommonCryptor.h>
35 #include <CoreFoundation/CFBase.h>
41 #include <utilities/SecCFRelease.h>
42 #include <utilities/debugging.h>
44 CFStringRef kSecRecVersionNumber
= CFSTR("SRVersionNumber");
45 CFStringRef kSecRecQuestions
= CFSTR("SRQuestions");
46 CFStringRef kSecRecLocale
= CFSTR("SRLocale");
47 CFStringRef kSecRecIV
= CFSTR("SRiv");
48 CFStringRef kSecRecWrappedPassword
= CFSTR("SRWrappedPassword");
51 static char *std_log_prefix
= "###SecRecovery Function: %s - %s";
52 static const char *std_ident
= "Security.framework";
53 static const char *std_facility
= "InfoSec";
54 static uint32_t std_options
= 0;
56 static aslclient aslhandle
= NULL
;
57 static aslmsg msgptr
= NULL
;
61 void ccdebug_imp(int level
, char *funcname
, char *format
, ...);
63 #define secDebug(lvl,fmt,...) sec_debug_imp(lvl, __PRETTY_FUNCTION__, fmt, __VA_ARGS__)
67 char *ccEnvStdErr
= getenv("CC_STDERR");
69 if(ccEnvStdErr
!= NULL
&& strncmp(ccEnvStdErr
, "yes", 3) == 0) std_options
|= ASL_OPT_STDERR
;
70 aslhandle
= asl_open(std_ident
, std_facility
, std_options
);
72 msgptr
= asl_new(ASL_TYPE_MSG
);
73 asl_set(msgptr
, ASL_KEY_FACILITY
, "com.apple.infosec");
78 sec_debug_imp(int level
, const char *funcname
, char *format
, ...) {
82 if(aslhandle
== NULL
) sec_debug_init();
84 sprintf(fmtbuffer
, std_log_prefix
, funcname
, format
);
85 va_start(argp
, format
);
86 asl_vlog(aslhandle
, msgptr
, level
, fmtbuffer
, argp
);
90 // Read /dev/random for random bytes
93 createRandomBytes(size_t len
)
95 CFMutableDataRef data
= CFDataCreateMutable(NULL
, len
);
98 CFDataSetLength(data
, len
);
99 if (SecRandomCopyBytes(kSecRandomDefault
, len
, CFDataGetMutableBytePtr(data
)) != noErr
) {
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 CF_RETURNS_RETAINED
135 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 createDigestString(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 CFStringRef errorReason
= CFErrorCopyFailureReason(error
);
227 secDebug(ASL_LEVEL_ERR
, "Couldn't create digest %s\n", CFStringGetCStringPtr(errorReason
, kCFStringEncodingUTF8
));
228 CFReleaseNull(errorReason
);
232 CFReleaseNull(digestTrans
);
233 CFReleaseNull(inputString
);
237 static CFDataRef CF_RETURNS_RETAINED
238 b64encode(CFDataRef input
)
240 CFDataRef retval
= NULL
;
241 CFErrorRef error
= NULL
;
242 SecTransformRef encodeTrans
= SecEncodeTransformCreate(kSecBase64Encoding
, &error
);
243 if(error
== NULL
) SecTransformSetAttribute(encodeTrans
, kSecTransformInputAttributeName
, input
, &error
);
244 if(error
== NULL
) retval
= SecTransformExecute(encodeTrans
, &error
);
245 if(encodeTrans
) CFRelease(encodeTrans
);
249 static CFDataRef CF_RETURNS_RETAINED
250 b64decode(CFDataRef input
)
252 CFDataRef retval
= NULL
;
253 CFErrorRef error
= NULL
;
254 SecTransformRef decodeTrans
= SecDecodeTransformCreate(kSecBase64Encoding
, &error
);
255 if(error
== NULL
) SecTransformSetAttribute(decodeTrans
, kSecTransformInputAttributeName
, input
, &error
);
256 if(error
== NULL
) retval
= SecTransformExecute(decodeTrans
, &error
);
257 if(decodeTrans
) CFRelease(decodeTrans
);
261 static CFDataRef CF_RETURNS_RETAINED
262 encryptString(SecKeyRef wrapKey
, CFDataRef iv
, CFStringRef str
)
264 CFDataRef retval
= NULL
;
265 CFErrorRef error
= NULL
;
266 CFDataRef inputString
= CFStringCreateExternalRepresentation(kCFAllocatorDefault
, str
, kCFStringEncodingMacRoman
, 0xff);
267 SecTransformRef encrypt
= NULL
;
268 SecTransformRef encode
= NULL
;
269 SecTransformRef group
= NULL
;
271 encrypt
= SecEncryptTransformCreate(wrapKey
, &error
);
273 SecTransformSetAttribute(encrypt
, kSecEncryptionMode
, kSecModeCBCKey
, &error
);
275 SecTransformSetAttribute(encrypt
, kSecPaddingKey
, kSecPaddingPKCS7Key
, &error
);
277 SecTransformSetAttribute(encrypt
, kSecTransformInputAttributeName
, inputString
, &error
);
279 SecTransformSetAttribute(encrypt
, kSecIVKey
, iv
, &error
);
282 encode
= SecEncodeTransformCreate(kSecBase64Encoding
, &error
);
285 group
= SecTransformCreateGroupTransform();
286 SecTransformConnectTransforms(encrypt
, kSecTransformOutputAttributeName
, encode
, kSecTransformInputAttributeName
, group
, &error
);
288 retval
= SecTransformExecute(group
, &error
);
293 secerror("Failed to encrypt recovery password: %@", error
);
296 CFReleaseNull(error
);
297 CFReleaseNull(inputString
);
298 CFReleaseNull(encrypt
);
299 CFReleaseNull(encode
);
300 CFReleaseNull(group
);
306 static CFStringRef CF_RETURNS_RETAINED
307 decryptString(SecKeyRef wrapKey
, CFDataRef iv
, CFDataRef wrappedPassword
)
309 CFStringRef retval
= NULL
;
310 CFDataRef retData
= NULL
;
311 CFErrorRef error
= NULL
;
312 SecTransformRef decode
= NULL
;
313 SecTransformRef decrypt
= NULL
;
314 SecTransformRef group
= NULL
;
316 decode
= SecDecodeTransformCreate(kSecBase64Encoding
, &error
);
318 SecTransformSetAttribute(decode
, kSecTransformInputAttributeName
, wrappedPassword
, &error
);
321 decrypt
= SecDecryptTransformCreate(wrapKey
, &error
);
323 SecTransformSetAttribute(decrypt
, kSecEncryptionMode
, kSecModeCBCKey
, &error
);
325 SecTransformSetAttribute(decrypt
, kSecPaddingKey
, kSecPaddingPKCS7Key
, &error
);
327 SecTransformSetAttribute(decrypt
, kSecIVKey
, iv
, &error
);
330 group
= SecTransformCreateGroupTransform();
331 SecTransformConnectTransforms(decode
, kSecTransformOutputAttributeName
, decrypt
, kSecTransformInputAttributeName
, group
, &error
);
333 retData
= SecTransformExecute(group
, &error
);
335 retval
= CFStringCreateFromExternalRepresentation(kCFAllocatorDefault
, retData
, kCFStringEncodingMacRoman
);
339 secerror("Failed to decrypt recovery password: %@", error
);
342 CFReleaseNull(retData
);
343 CFReleaseNull(error
);
344 CFReleaseNull(decode
);
345 CFReleaseNull(decrypt
);
346 CFReleaseNull(group
);
351 // IV for the recovery ref is currently the leftmost 16 bytes of the digest of the recovery password.
352 #define IVBYTECOUNT 16
355 createIVFromPassword(CFStringRef password
)
357 CFDataRef hashedPassword
, retval
;
359 if((hashedPassword
= createDigestString(password
)) == NULL
) return NULL
;
360 iv
= CFDataCreateMutableCopy(kCFAllocatorDefault
, CFDataGetLength(hashedPassword
)+1, hashedPassword
);
361 CFDataDeleteBytes(iv
, CFRangeMake(IVBYTECOUNT
, CFDataGetLength(iv
)-IVBYTECOUNT
));
362 retval
= CFDataCreateCopy(kCFAllocatorDefault
, iv
);
363 CFRelease(hashedPassword
);
374 * Function: SecWrapRecoveryPasswordWithAnswers
375 * Description: This will wrap a password by using answers to generate a key. The resulting
376 * wrapped password and the questions used to get the answers are saved in a
377 * recovery dictionary.
380 CFDictionaryRef CF_RETURNS_RETAINED
381 SecWrapRecoveryPasswordWithAnswers(CFStringRef password
, CFArrayRef questions
, CFArrayRef answers
)
385 CFDataRef wrappedPassword
= NULL
;
386 CFMutableDictionaryRef retval
= NULL
;
387 CFLocaleRef theLocale
= CFLocaleCopyCurrent();
388 CFStringRef theLocaleString
= CFLocaleGetIdentifier(theLocale
);
389 SecKeyRef wrapKey
= NULL
;
393 if (!password
|| !questions
|| !answers
) {
397 limit
= CFArrayGetCount(answers
);
398 if (limit
!= CFArrayGetCount(questions
)) {
402 for (ix
=0; ix
<limit
; ix
++)
404 chkval
= CFArrayGetValueAtIndex(answers
, ix
);
405 if (!chkval
|| CFGetTypeID(chkval
)!=CFStringGetTypeID() || CFEqual((CFStringRef
)chkval
, CFSTR(""))) {
408 chkval
= CFArrayGetValueAtIndex(questions
, ix
);
409 if (!chkval
|| CFGetTypeID(chkval
)!=CFStringGetTypeID() || CFEqual((CFStringRef
)chkval
, CFSTR(""))) {
414 iv
= createIVFromPassword(password
);
416 wrapKey
= secDeriveKeyFromAnswers(answers
, theLocale
);
418 if((wrappedPassword
= encryptString(wrapKey
, iv
, password
)) != NULL
) {
419 retval
= CFDictionaryCreateMutable(kCFAllocatorDefault
, 5, &kCFTypeDictionaryKeyCallBacks
, &kCFTypeDictionaryValueCallBacks
);
420 CFNumberRef num
= CFNumberCreate(kCFAllocatorDefault
, kCFNumberSInt32Type
, &vers
);
421 CFDictionaryAddValue(retval
, kSecRecVersionNumber
, num
);
423 CFDictionaryAddValue(retval
, kSecRecQuestions
, questions
);
424 CFDictionaryAddValue(retval
, kSecRecLocale
, theLocaleString
);
425 CFDataRef ivdata
= b64encode(iv
);
426 CFDictionaryAddValue(retval
, kSecRecIV
, ivdata
);
427 CFReleaseNull(ivdata
);
428 CFDictionaryAddValue(retval
, kSecRecWrappedPassword
, wrappedPassword
);
431 CFReleaseNull(wrappedPassword
);
434 CFReleaseNull(retval
);
437 CFReleaseNull(wrapKey
);
438 CFReleaseNull(theLocale
);
444 * Function: SecUnwrapRecoveryPasswordWithAnswers
445 * Description: This will unwrap a password contained in a recovery dictionary by using answers
449 CFStringRef CF_RETURNS_RETAINED
450 SecUnwrapRecoveryPasswordWithAnswers(CFDictionaryRef recref
, CFArrayRef answers
)
452 if(answers
== NULL
|| CFArrayGetCount(answers
) < 3) return NULL
;
454 CFStringRef theLocaleString
= (CFStringRef
) CFDictionaryGetValue(recref
, kSecRecLocale
);
455 CFDataRef tmpIV
= (CFDataRef
) CFDictionaryGetValue(recref
, kSecRecIV
);
456 CFDataRef wrappedPassword
= (CFDataRef
) CFDictionaryGetValue(recref
, kSecRecWrappedPassword
);
458 if(theLocaleString
== NULL
|| tmpIV
== NULL
|| wrappedPassword
== NULL
) {
463 CFLocaleRef theLocale
= CFLocaleCreate(kCFAllocatorDefault
, theLocaleString
);
464 SecKeyRef wrapKey
= secDeriveKeyFromAnswers(answers
, theLocale
);
465 CFRelease(theLocale
);
467 CFDataRef iv
= b64decode(tmpIV
);
469 CFStringRef recoveryPassword
= decryptString(wrapKey
, iv
, wrappedPassword
);
472 if(recoveryPassword
!= NULL
) {
473 CFDataRef comphash
= createIVFromPassword(recoveryPassword
);
474 if(!CFEqual(comphash
, iv
)) {
475 secDebug(ASL_LEVEL_ERR
, "Failed reconstitution of password for recovery\n", NULL
);
476 CFRelease(recoveryPassword
);
477 recoveryPassword
= NULL
;
482 return recoveryPassword
;
486 * Function: SecCreateRecoveryPassword
487 * Description: This function will get a random 128 bit number and base32
488 * encode and format that value
492 SecCreateRecoveryPassword(void)
494 CFStringRef result
= NULL
;
495 CFErrorRef error
= NULL
;
496 CFDataRef encodedData
= NULL
;
497 CFDataRef randData
= createRandomBytes(16);
500 // base32FDE is a "private" base32 encoding, it has no 0/O or L/l/1 in it (it uses 8 and 9).
501 SecTransformRef encodeTrans
= SecEncodeTransformCreate(CFSTR("base32FDE"), &error
);
503 SecTransformSetAttribute(encodeTrans
, kSecTransformInputAttributeName
, randData
, &error
);
504 if(error
== NULL
) encodedData
= SecTransformExecute(encodeTrans
, &error
);
506 CFReleaseNull(encodeTrans
);
509 if(encodedData
!= NULL
&& error
== NULL
) {
510 CFStringRef b32string
= CFStringCreateFromExternalRepresentation(kCFAllocatorDefault
, encodedData
, kCFStringEncodingMacRoman
);
511 CFMutableStringRef encodedString
= CFStringCreateMutableCopy(kCFAllocatorDefault
, 64, b32string
);
513 // Add some hyphens to make the generated password easier to use
514 for(i
= 4; i
< 34; i
+= 5) CFStringInsert(encodedString
, i
, CFSTR("-"));
515 // Trim so the last section is 4 characters long
516 CFStringDelete(encodedString
, CFRangeMake(29,CFStringGetLength(encodedString
)-29));
517 result
= CFStringCreateCopy(kCFAllocatorDefault
, encodedString
);
518 CFRelease(encodedString
);
519 CFRelease(b32string
);
521 secDebug(ASL_LEVEL_ERR
, "Failed to base32 encode random data for recovery password\n", NULL
);
523 CFReleaseNull(encodedData
);