keychain_export.m   [plain text]

/*
* Copyright (c) 2003-2004,2006,2012,2014 Apple Inc. All Rights Reserved.
*
* @APPLE_LICENSE_HEADER_START@
*
* This file contains Original Code and/or Modifications of Original Code
* as defined in and that are subject to the Apple Public Source License
* Version 2.0 (the 'License'). You may not use this file except in
* compliance with the License. Please obtain a copy of the License at
* http://www.opensource.apple.com/apsl/ and read it before using this
* file.
*
* The Original Code and all software distributed under the License are
* distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
* EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
* INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
* Please see the License for the specific language governing rights and
* limitations under the License.
*
* @APPLE_LICENSE_HEADER_END@
*
* keychain_export.c
*/

#import <Foundation/Foundation.h>

#include "keychain_export.h"
#include "keychain_utilities.h"
#include "security_tool.h"

#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <Security/SecImportExport.h>
#include <Security/SecKeychainItem.h>
#include <Security/SecKeychainSearch.h>
#include <Security/SecIdentitySearch.h>
#include <Security/SecKey.h>
#include <Security/SecKeyPriv.h>
#include <Security/SecCertificate.h>
#include <Security/SecCertificatePriv.h>
#include <Security/SecItem.h>
#include <Security/SecAccessControl.h>
#include <Security/SecAccessControlPriv.h>
#include <security_cdsa_utils/cuFileIo.h>
#include <CoreFoundation/CoreFoundation.h>
#include <stdio.h>

typedef enum {
IS_Certs,
IS_AllKeys,
IS_PubKeys,
IS_PrivKeys,
IS_Identities,
IS_All
} ItemSpec;

/*
* Add all itmes of specified class from a keychain to an array.
* Item class are things like kSecCertificateItemClass, and
* CSSM_DL_DB_RECORD_PRIVATE_KEY. Identities are searched separately.
*/
static OSStatus addKcItems(
SecKeychainRef kcRef,
SecItemClass itemClass, // kSecCertificateItemClass
CFMutableArrayRef outArray,
unsigned *numItems) // UPDATED on return
{
OSStatus ortn;
SecKeychainSearchRef srchRef;

ortn = SecKeychainSearchCreateFromAttributes(kcRef,
itemClass,
NULL, // no attrs
&srchRef);
if(ortn) {
sec_perror("SecKeychainSearchCreateFromAttributes", ortn);
return ortn;
}
for(;;) {
SecKeychainItemRef itemRef;
ortn = SecKeychainSearchCopyNext(srchRef, &itemRef);
if(ortn) {
if(ortn == errSecItemNotFound) {
/* normal search end */
ortn = noErr;
}
else {
sec_perror("SecIdentitySearchCopyNext", ortn);
}
break;
}
CFArrayAppendValue(outArray, itemRef);
CFRelease(itemRef); // array owns the item
(*numItems)++;
}
CFRelease(srchRef);
return ortn;
}

/*
* Add all SecIdentityRefs from a keychain into an array.
*/
static OSStatus addIdentities(
SecKeychainRef kcRef,
CFMutableArrayRef outArray,
unsigned *numItems) // UPDATED on return
{
/* Search for all identities */
SecIdentitySearchRef srchRef;
OSStatus ortn = SecIdentitySearchCreate(kcRef,
0, // keyUsage - any
&srchRef);
if(ortn) {
sec_perror("SecIdentitySearchCreate", ortn);
return ortn;
}

do {
SecIdentityRef identity;
ortn = SecIdentitySearchCopyNext(srchRef, &identity);
if(ortn) {
if(ortn == errSecItemNotFound) {
/* normal search end */
ortn = noErr;
}
else {
sec_perror("SecIdentitySearchCopyNext", ortn);
}
break;
}
CFArrayAppendValue(outArray, identity);

/* the array has the retain count we need */
CFRelease(identity);
(*numItems)++;
} while(ortn == noErr);
CFRelease(srchRef);
return ortn;
}

static int do_keychain_export(
SecKeychainRef kcRef,
SecExternalFormat externFormat,
ItemSpec itemSpec,
const char *passphrase,
int doPem,
const char *fileName)
{
int result = 0;
CFIndex numItems;
unsigned numPrivKeys = 0;
unsigned numPubKeys = 0;
unsigned numCerts = 0;
unsigned numIdents = 0;
OSStatus ortn;
uint32 expFlags = 0; // SecItemImportExportFlags
SecKeyImportExportParameters keyParams;
CFStringRef passStr = NULL;
CFDataRef outData = NULL;
size_t len;

/* gather items */
CFMutableArrayRef exportItems = CFArrayCreateMutable(NULL, 0,
&kCFTypeArrayCallBacks);
switch(itemSpec) {
case IS_Certs:
ortn = addKcItems(kcRef, kSecCertificateItemClass, exportItems, &numCerts);
if(ortn) {
result = 1;
goto loser;
}
break;

case IS_PrivKeys:
ortn = addKcItems(kcRef, CSSM_DL_DB_RECORD_PRIVATE_KEY, exportItems,
&numPrivKeys);
if(ortn) {
result = 1;
goto loser;
}
break;

case IS_PubKeys:
ortn = addKcItems(kcRef, CSSM_DL_DB_RECORD_PUBLIC_KEY, exportItems,
&numPubKeys);
if(ortn) {
result = 1;
goto loser;
}
break;

case IS_AllKeys:
ortn = addKcItems(kcRef, CSSM_DL_DB_RECORD_PRIVATE_KEY, exportItems,
&numPrivKeys);
if(ortn) {
result = 1;
goto loser;
}
ortn = addKcItems(kcRef, CSSM_DL_DB_RECORD_PUBLIC_KEY, exportItems,
&numPubKeys);
if(ortn) {
result = 1;
goto loser;
}
break;

case IS_All:
/* No public keys here - PKCS12 doesn't support them */
ortn = addKcItems(kcRef, kSecCertificateItemClass, exportItems, &numCerts);
if(ortn) {
result = 1;
goto loser;
}
ortn = addKcItems(kcRef, CSSM_DL_DB_RECORD_PRIVATE_KEY, exportItems,
&numPrivKeys);
if(ortn) {
result = 1;
goto loser;
}
break;

case IS_Identities:
ortn = addIdentities(kcRef, exportItems, &numIdents);
if(ortn) {
result = 1;
goto loser;
}
if(numIdents) {
numPrivKeys += numIdents;
numCerts += numIdents;
}
break;
default:
sec_error("Internal error parsing item_spec");
result = 1;
goto loser;
}

numItems = CFArrayGetCount(exportItems);
if(externFormat == kSecFormatUnknown) {
/* Use default export format per set of items */
if(numItems > 1) {
externFormat = kSecFormatPEMSequence;
}
else if(numCerts) {
externFormat = kSecFormatX509Cert;
}
else {
externFormat = kSecFormatOpenSSL;
}
}
if(doPem) {
expFlags |= kSecItemPemArmour;
}

/*
* Key related arguments, ignored if we're not exporting keys.
* Always specify some kind of passphrase - default is secure passkey.
*/
memset(&keyParams, 0, sizeof(keyParams));
keyParams.version = SEC_KEY_IMPORT_EXPORT_PARAMS_VERSION;
if(passphrase != NULL) {
passStr = CFStringCreateWithCString(NULL, passphrase, kCFStringEncodingASCII);
keyParams.passphrase = passStr;
}
else {
keyParams.flags = kSecKeySecurePassphrase;
}

/* Go */
ortn = SecKeychainItemExport(exportItems, externFormat, expFlags, &keyParams,
&outData);
if(ortn) {
sec_perror("SecKeychainItemExport", ortn);
result = 1;
goto loser;
}

len = CFDataGetLength(outData);
if(fileName) {
int rtn = writeFileSizet(fileName, CFDataGetBytePtr(outData), len);
if(rtn == 0) {
if(!do_quiet) {
fprintf(stderr, "...%lu bytes written to %s\n", len, fileName);
}
}
else {
sec_error("Error writing to %s: %s", fileName, strerror(errno));
result = 1;
}
}
else {
size_t irtn = write(STDOUT_FILENO, CFDataGetBytePtr(outData), len);
if(irtn != (size_t)len) {
perror("write");
}
}
loser:
if(exportItems) {
CFRelease(exportItems);
}
if(passStr) {
CFRelease(passStr);
}
if(outData) {
CFRelease(outData);
}
return result;
}

int
keychain_export(int argc, char * const *argv)
{
int ch, result = 0;

char *outFile = NULL;
char *kcName = NULL;
SecKeychainRef kcRef = NULL;
SecExternalFormat externFormat = kSecFormatUnknown;
ItemSpec itemSpec = IS_All;
int wrapped = 0;
int doPem = 0;
const char *passphrase = NULL;

while ((ch = getopt(argc, argv, "k:o:t:f:P:wph")) != -1)
{
switch (ch)
{
case 'k':
kcName = optarg;
break;
case 'o':
outFile = optarg;
break;
case 't':
if(!strcmp("certs", optarg)) {
itemSpec = IS_Certs;
}
else if(!strcmp("allKeys", optarg)) {
itemSpec = IS_AllKeys;
}
else if(!strcmp("pubKeys", optarg)) {
itemSpec = IS_PubKeys;
}
else if(!strcmp("privKeys", optarg)) {
itemSpec = IS_PrivKeys;
}
else if(!strcmp("identities", optarg)) {
itemSpec = IS_Identities;
}
else if(!strcmp("all", optarg)) {
itemSpec = IS_All;
}
else {
return SHOW_USAGE_MESSAGE;
}
break;
case 'f':
if(!strcmp("openssl", optarg)) {
externFormat = kSecFormatOpenSSL;
}
else if(!strcmp("openssh1", optarg)) {
externFormat = kSecFormatSSH;
}
else if(!strcmp("openssh2", optarg)) {
externFormat = kSecFormatSSHv2;
}
else if(!strcmp("bsafe", optarg)) {
externFormat = kSecFormatBSAFE;
}
else if(!strcmp("raw", optarg)) {
externFormat = kSecFormatRawKey;
}
else if(!strcmp("pkcs7", optarg)) {
externFormat = kSecFormatPKCS7;
}
else if(!strcmp("pkcs8", optarg)) {
externFormat = kSecFormatWrappedPKCS8;
}
else if(!strcmp("pkcs12", optarg)) {
externFormat = kSecFormatPKCS12;
}
else if(!strcmp("netscape", optarg)) {
externFormat = kSecFormatNetscapeCertSequence;
}
else if(!strcmp("x509", optarg)) {
externFormat = kSecFormatX509Cert;
}
else if(!strcmp("pemseq", optarg)) {
externFormat = kSecFormatPEMSequence;
}
else {
return SHOW_USAGE_MESSAGE;
}
break;
case 'w':
wrapped = 1;
break;
case 'p':
doPem = 1;
break;
case 'P':
passphrase = optarg;
break;
case '?':
default:
return SHOW_USAGE_MESSAGE;
}
}

if(wrapped) {
switch(externFormat) {
case kSecFormatOpenSSL:
case kSecFormatUnknown: // i.e., use default
externFormat = kSecFormatWrappedOpenSSL;
break;
case kSecFormatSSH:
externFormat = kSecFormatWrappedSSH;
break;
case kSecFormatSSHv2:
/* there is no wrappedSSHv2 */
externFormat = kSecFormatWrappedOpenSSL;
break;
case kSecFormatWrappedPKCS8:
/* proceed */
break;
default:
sec_error("Don't know how to wrap in specified format/type");
return SHOW_USAGE_MESSAGE;
}
}

if(kcName) {
kcRef = keychain_open(kcName);
if(kcRef == NULL) {
return 1;
}
}
result = do_keychain_export(kcRef, externFormat, itemSpec,
passphrase, doPem, outFile);

if(kcRef) {
CFRelease(kcRef);
}
return result;
}

typedef struct {
CFMutableStringRef str;
} ctk_dict2str_context;

static void
ctk_obj_to_str(CFTypeRef obj, char *buf, int bufLen, Boolean key);

static void
ctk_dict2str(const void *key, const void *value, void *context)
{
char keyBuf[64] = { 0 };
ctk_obj_to_str(key, keyBuf, sizeof(keyBuf), true);

char valueBuf[1024] = { 0 };
ctk_obj_to_str(value, valueBuf, sizeof(valueBuf), false);

CFStringRef str = CFStringCreateWithFormat(kCFAllocatorDefault, NULL, CFSTR("\n\t\t\t%s : %s,"), keyBuf, valueBuf);
CFStringAppend(((ctk_dict2str_context *)context)->str, str);
CFRelease(str);
}

static void
ctk_obj_to_str(CFTypeRef obj, char *buf, int bufLen, Boolean key)
{
CFStringRef str = NULL;

if(CFGetTypeID(obj) == CFStringGetTypeID()) {
// CFStringRef - print the string as is (for keys) or quoted (values)
str = CFStringCreateWithFormat(kCFAllocatorDefault, NULL, key ? CFSTR("%@") : CFSTR("\"%@\""), obj);
} else if(CFGetTypeID(obj) == CFNumberGetTypeID()) {
// CFNumber - print the value using current locale
CFNumberRef num = (CFNumberRef)obj;

CFLocaleRef locale = CFLocaleCopyCurrent();
CFNumberFormatterRef fmt = CFNumberFormatterCreate(kCFAllocatorDefault, locale, kCFNumberFormatterDecimalStyle);
CFRelease(locale);

str = CFNumberFormatterCreateStringWithNumber(kCFAllocatorDefault, fmt, num);
CFRelease(fmt);
} else if(CFGetTypeID(obj) == CFDataGetTypeID()) {
// CFData - print the data as <hex bytes>
CFDataRef data = (CFDataRef)obj;

CFMutableStringRef hexStr = CFStringCreateMutable(kCFAllocatorDefault, CFDataGetLength(data) * 3);

for(int i = 0; i < CFDataGetLength(data); i++) {
CFStringRef hexByte = CFStringCreateWithFormat(kCFAllocatorDefault, NULL, CFSTR("%02x "), *(CFDataGetBytePtr(data) + i));
CFStringAppend(hexStr, hexByte);
CFRelease(hexByte);
}

// Get rid of the last excessive space.
if(CFDataGetLength(data)) {
CFStringDelete(hexStr, CFRangeMake(CFStringGetLength(hexStr) - 1, 1));
}

str = CFStringCreateWithFormat(kCFAllocatorDefault, NULL, CFSTR("<%@>"), hexStr);
CFRelease(hexStr);
} else if(CFGetTypeID(obj) == CFBooleanGetTypeID()) {
// CFBoolean - print true/false
CFBooleanRef cfbool = (CFBooleanRef)obj;

str = CFStringCreateWithCString(kCFAllocatorDefault, CFBooleanGetValue(cfbool) ? "true" : "false", kCFStringEncodingUTF8);
} else if(CFGetTypeID(obj) == SecAccessControlGetTypeID()) {
// SecAccessControlRef - print the constraints dictionary
SecAccessControlRef ac = (SecAccessControlRef)obj;

CFDictionaryRef constraints = SecAccessControlGetConstraints(ac);
CFMutableStringRef constraintsStr = CFStringCreateMutable(kCFAllocatorDefault, 1024);
if(constraints && CFDictionaryGetCount(constraints)) {
ctk_dict2str_context context;
context.str = constraintsStr;
CFDictionaryApplyFunction(constraints, ctk_dict2str, &context);
CFStringReplace(constraintsStr, CFRangeMake(CFStringGetLength(constraintsStr) - 1, 1), CFSTR("\n\t\t"));
}

CFDictionaryRef protection = SecAccessControlGetProtection(ac);
CFMutableStringRef protectionStr = CFStringCreateMutable(kCFAllocatorDefault, 512);
if(protection && CFDictionaryGetCount(protection)) {
ctk_dict2str_context context;
context.str = protectionStr;
CFDictionaryApplyFunction(protection, ctk_dict2str, &context);
CFStringReplace(protectionStr, CFRangeMake(CFStringGetLength(protectionStr) - 1, 1), CFSTR("\n\t\t"));
}

str = CFStringCreateWithFormat(kCFAllocatorDefault, NULL, CFSTR("constraints: {%@}\n\t\tprotection: {%@}"), constraintsStr, protectionStr);
CFRelease(constraintsStr);
CFRelease(protectionStr);
}

// Fill the provided buffer with the converted string.
if(str) {
Boolean success = CFStringGetCString(str, buf, bufLen, kCFStringEncodingUTF8);
CFRelease(str);

if(success) {
return;
}
}

// Use object description as fallback...
CFStringRef description = CFCopyDescription(obj);
if(!CFStringGetCString(description, buf, bufLen, kCFStringEncodingUTF8)) {
// ...or else we don't know.
strncpy(buf, "<?>", bufLen);
}

CFRelease(description);
}

typedef struct {
int i;
NSString *name;
} ctk_print_context;

OSStatus
ctk_dump_item(CFTypeRef item, ctk_print_context *ctx);

static void
ctk_print_dict(const void *key, const void *value, void *context)
{
char keyBuf[64] = { 0 };
ctk_obj_to_str(key, keyBuf, sizeof(keyBuf), true);

char valueBuf[1024] = { 0 };
ctk_obj_to_str(value, valueBuf, sizeof(valueBuf), false);

printf("\t%s : %s\n", keyBuf, valueBuf);
}

static void
ctk_dump_item_header(ctk_print_context *ctx)
{
printf("\n");
printf("==== %s #%d\n", ctx->name.UTF8String, ctx->i);
}

static void
ctk_dump_item_footer(ctk_print_context *ctx)
{
printf("====\n");
}

OSStatus
ctk_dump_item(CFTypeRef item, ctk_print_context *ctx)
{
OSStatus stat = errSecSuccess;

CFTypeID tid = CFGetTypeID(item);
if(tid == CFDictionaryGetTypeID()) {
// We expect a dictionary containing item attributes.
ctk_dump_item_header(ctx);
CFDictionaryApplyFunction((CFDictionaryRef)item, ctk_print_dict, ctx);
ctk_dump_item_footer(ctx);
} else {
stat = errSecInternalComponent;
printf("Unexpected item type ID: %lu\n", tid);
}

return stat;
}

static OSStatus
ctk_dump_items(CFArrayRef items, id secClass, NSString *name)
{
OSStatus stat = errSecSuccess;

ctk_print_context ctx = { 1, name };

for(CFIndex i = 0; i < CFArrayGetCount(items); i++) {
CFTypeRef item = CFArrayGetValueAtIndex(items, i);
stat = ctk_dump_item(item, &ctx);
ctx.i++;

if(stat) {
break;
}
}

return stat;
}

static void
exportData(NSData *dataToExport, NSString *fileName, NSString *exportPath, NSString *elementName) {
NSMutableString *pem = [NSMutableString new];
[pem appendString:[NSString stringWithFormat:@"-----BEGIN %@-----\n", elementName]];
NSString *base64Cert = [dataToExport base64EncodedStringWithOptions:NSDataBase64Encoding64CharacterLineLength | NSDataBase64EncodingEndLineWithLineFeed];
[pem appendString:base64Cert];
[pem appendString:[NSString stringWithFormat:@"\n-----END %@-----\n", elementName]];

NSString *fullName = [NSString stringWithFormat:@"%@/%@.pem", exportPath, fileName];
NSError *error;
[pem writeToFile:fullName atomically:YES encoding:NSUTF8StringEncoding error:&error];
if (error) {
fprintf(stderr, "%s\n", [NSString stringWithFormat:@"%@", error].UTF8String);
}
}

static OSStatus
ctk_dump(id secClass, NSString *name, NSString *tid, NSString *exportPath)
{
NSArray *result;
BOOL returnRef = NO;

if ([secClass isEqual:(id)kSecClassIdentity] || [secClass isEqual:(id)kSecClassCertificate])
returnRef = YES;

NSDictionary *query = @{
(id)kSecClass : secClass,
(id)kSecMatchLimit : (id)kSecMatchLimitAll,
(id)kSecAttrAccessGroup : (id)kSecAttrAccessGroupToken,
(id)kSecReturnAttributes : @YES,
(id)kSecReturnRef : @(returnRef)
};

if(tid) {
NSMutableDictionary *updatedQuery = [NSMutableDictionary dictionaryWithDictionary:query];
updatedQuery[(id)kSecAttrTokenID] = tid;
query = updatedQuery;
}

OSStatus stat = SecItemCopyMatching((__bridge CFTypeRef)query, (void *)&result);
if(stat) {
if (stat == errSecItemNotFound) {
fprintf(stderr, "No items found.\n");
} else {
sec_error("SecItemCopyMatching: %x (%d) - %s", stat, stat, sec_errstr(stat));
}
return stat;
}

// We expect an array of dictionaries containing item attributes as result.
if([result isKindOfClass:[NSArray class]]) {
if (returnRef) {
NSMutableArray *updatedResult = [NSMutableArray array];
for (NSDictionary *dict in result) {
NSMutableDictionary *updatedItem = [NSMutableDictionary dictionaryWithDictionary:dict];
id itemRef = updatedItem[(id)kSecValueRef];
if ([secClass isEqual:(id)kSecClassIdentity]) {
id certificateRef;
if (SecIdentityCopyCertificate((__bridge SecIdentityRef)itemRef, (void *)&certificateRef) != errSecSuccess)
continue;
itemRef = certificateRef;
}

NSData *certDigest = (__bridge NSData*)SecCertificateGetSHA1Digest((__bridge SecCertificateRef)itemRef);
updatedItem[@"sha1"] = certDigest;
[updatedItem removeObjectForKey:(id)kSecValueRef];
[updatedResult addObject:updatedItem];

if (exportPath) {
NSData *certData = (__bridge_transfer NSData *)SecCertificateCopyData((__bridge SecCertificateRef)itemRef);
exportData(certData, updatedItem[(id)kSecAttrLabel], exportPath, @"CERTIFICATE");
id publicKey = (__bridge_transfer id)SecCertificateCopyKey((__bridge SecCertificateRef)itemRef);
NSData *pubKeyInfo = (__bridge_transfer NSData *)SecKeyCopySubjectPublicKeyInfo((__bridge SecKeyRef)publicKey);
exportData(pubKeyInfo, [NSString stringWithFormat:@"Public Key - %@", updatedItem[(id)kSecAttrLabel]], exportPath, @"PUBLIC KEY");
}
}
result = updatedResult;
}

if (!exportPath) {
stat = ctk_dump_items((__bridge CFArrayRef)result, secClass, name);
}
} else {
stat = errSecInternalComponent;
}
return stat;
}

int
ctk_export(int argc, char * const *argv)
{
__block OSStatus stat = errSecSuccess;

ItemSpec itemSpec = IS_All;
NSString *tid;
NSString *exportPath;
int ch;
BOOL optT = NO;
BOOL optE = NO;

while ((ch = getopt(argc, argv, "i:t:e:h")) != -1) {
switch (ch) {
case 't':
if(!strcmp("certs", optarg)) {
itemSpec = IS_Certs;
}
else if(!strcmp("privKeys", optarg)) {
itemSpec = IS_PrivKeys;
}
else if(!strcmp("identities", optarg)) {
itemSpec = IS_Identities;
}
else if(!strcmp("all", optarg)) {
itemSpec = IS_All;
}
else {
return SHOW_USAGE_MESSAGE;
}
optT = YES;
break;
case 'i':
tid = [NSString stringWithUTF8String:optarg];
break;
case 'e':
exportPath = [NSString stringWithUTF8String:optarg];
itemSpec = IS_Certs;
optE = YES;
break;

case '?':
default:
return SHOW_USAGE_MESSAGE;
}
}

if (optT && optE) {
return SHOW_USAGE_MESSAGE;
}

NSDictionary<id, NSArray *> *classesAndNames = @{ (id)kSecClassCertificate : @[ @"certificate", @(IS_Certs) ],
(id)kSecClassKey : @[ @"private key", @(IS_PrivKeys) ],
(id)kSecClassIdentity : @[ @"identity", @(IS_Identities) ] };

[classesAndNames enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, NSArray * _Nonnull obj, BOOL * _Nonnull stop) {
if (itemSpec == IS_All || itemSpec == ((NSNumber *)obj[1]).unsignedIntegerValue) {
stat = ctk_dump(key, obj[0], tid, exportPath);
if(stat) {
*stop = YES;
}
}
}];

return stat;
}