将RSA密钥导入iPhone钥匙串?

8

我有一对NSString对象,表示一个RSA公私钥对(不是由SecKeyCreatePair生成的,而是由外部加密库生成的)。如何从这些NSString对象创建SecKeyRef对象(SecKeyDecrypt / Encrypt方法所需的对象)?

我需要先将它们导入到Keychain中吗?如果需要,应该如何操作?

谢谢!


1
我们在一段时间前就已经解决了这个问题 - 如果传递正确的字典属性,SecItemAdd() 将会起作用。请参见 http://hg.mozilla.org/services/fx-home/file/tip/Sources/NetworkAndStorage/CryptoUtils.m#l931 - Anant
所有的魔法都在keyData参数中,你可以从某个地方(downloadPrivateKeyBundle)获取并解密它。这个NSData blob的格式是什么? - Uri London
5个回答

4
在iOS中,据我所知,密钥串是被沙箱保护的。这意味着你放入密钥串的任何内容只能由你自己的应用访问,除非你另外指定。你需要在项目设置的Capabilities下启用Keychain Sharing
既然解决了这个问题,你肯定可以导入数据。由于它们是NSString对象,因此您首先必须将其转换为NSData对象以正确导入它们。很可能它们是Base64编码,所以你需要将其反转:
NSData *decodedData = [[NSData alloc] initWithBase64EncodedString:base64String options:0];

现在,完成了上述步骤,您可以使用此方法将密钥保存到钥匙串并获取SecKeyRef:

/**
 * key: the data you're importing
 * keySize: the length of the key (512, 1024, 2048)
 * isPrivate: is this a private key or public key?
 */
- (SecKeyRef)saveKeyToKeychain:(NSData *)key keySize:(NSUInteger)keySize private:(BOOL)isPrivate {
    OSStatus sanityCheck = noErr;
    NSData *tag;
    id keyClass;

    if (isPrivate) {
        tag = privateTag;
        keyClass = (__bridge id) kSecAttrKeyClassPrivate;
    }
    else {
        tag = publicTag;
        keyClass = (__bridge id) kSecAttrKeyClassPublic;
    }

    NSDictionary *saveDict = @{
            (__bridge id) kSecClass : (__bridge id) kSecClassKey,
            (__bridge id) kSecAttrKeyType : (__bridge id) kSecAttrKeyTypeRSA,
            (__bridge id) kSecAttrApplicationTag : tag,
            (__bridge id) kSecAttrKeyClass : keyClass,
            (__bridge id) kSecValueData : key,
            (__bridge id) kSecAttrKeySizeInBits : [NSNumber numberWithUnsignedInteger:keySize],
            (__bridge id) kSecAttrEffectiveKeySize : [NSNumber numberWithUnsignedInteger:keySize],
            (__bridge id) kSecAttrCanDerive : (__bridge id) kCFBooleanFalse,
            (__bridge id) kSecAttrCanEncrypt : (__bridge id) kCFBooleanTrue,
            (__bridge id) kSecAttrCanDecrypt : (__bridge id) kCFBooleanFalse,
            (__bridge id) kSecAttrCanVerify : (__bridge id) kCFBooleanTrue,
            (__bridge id) kSecAttrCanSign : (__bridge id) kCFBooleanFalse,
            (__bridge id) kSecAttrCanWrap : (__bridge id) kCFBooleanTrue,
            (__bridge id) kSecAttrCanUnwrap : (__bridge id) kCFBooleanFalse
    };

    SecKeyRef savedKeyRef = NULL;
    sanityCheck = SecItemAdd((__bridge CFDictionaryRef) saveDict, (CFTypeRef *)&savedKeyRef);
    if (sanityCheck != errSecSuccess) {
        LOGGING_FACILITY1(sanityCheck != noErr, @"Problem saving the key to keychain, OSStatus == %d.", sanityCheck);
    }

    return savedKeyRef;
}

如果您稍后想要从钥匙串中检索SecKeyRef,则可以使用以下内容:

- (SecKeyRef)getKeyRef:(BOOL)isPrivate {
    OSStatus sanityCheck = noErr;
    NSData *tag;
    id keyClass;
    if (isPrivate) {
        if (privateKeyRef != NULL) {
            // already exists in memory, return
            return privateKeyRef;
        }
        tag = privateTag;
        keyClass = (__bridge id) kSecAttrKeyClassPrivate;
    }
    else {
        if (publicKeyRef != NULL) {
            // already exists in memory, return
            return publicKeyRef;
        }
        tag = publicTag;
        keyClass = (__bridge id) kSecAttrKeyClassPublic;
    }

    NSDictionary *queryDict = @{
            (__bridge id) kSecClass : (__bridge id) kSecClassKey,
            (__bridge id) kSecAttrKeyType : (__bridge id) kSecAttrKeyTypeRSA,
            (__bridge id) kSecAttrApplicationTag : tag,
            (__bridge id) kSecAttrKeyClass : keyClass,
            (__bridge id) kSecReturnRef : (__bridge id) kCFBooleanTrue
    };

    SecKeyRef keyReference = NULL;
    sanityCheck = SecItemCopyMatching((__bridge CFDictionaryRef) queryDict, (CFTypeRef *) &keyReference);
    if (sanityCheck != errSecSuccess) {
        NSLog(@"Error trying to retrieve key from server. isPrivate: %d. sanityCheck: %li", isPrivate, sanityCheck);
    }

    if (isPrivate) {
        privateKeyRef = keyReference;
    }
    else {
        publicKeyRef = keyReference;
    }
    return keyReference;
}

3

编辑:使用以下方法,我们能够导入大小为4096的密钥。任何比这更大的RSA密钥似乎都会被钥匙串拒绝。我们得到了成功状态,但我们没有获得密钥的引用。

关于导入RSA私钥/公钥的一些注意事项。在我的情况下,我需要导入由OpenSSL生成的私钥。

这个项目基本上实现了我想要的所有功能,可以将其放入钥匙串中。正如您所看到的,它只有一个keydata,您将密钥数据放入其中,然后钥匙串从密钥中找出块大小等信息。钥匙串支持ASN.1编码的密钥。

当您将密钥导出到文件时,它很可能是PEM文件。PEM文件只是一个base64编码的DER结构。该DER结构是一个广义结构,但在OpenSSL的情况下,它通常是一个ASN.1编码的私钥或公钥。

ASN.1结构在这里展示得非常好。请在尝试处理此内容或导入其他大小的密钥之前,请阅读并理解如何读取ASN.1结构。

我显然没有足够的“声望”来发布超过2个链接。因此,对于以下示例,请在lapo.it/asn1js中粘贴base64信息(除了--- BEGIN * KEY ---和---END * KEY ---之外的所有内容)。

如果您查看我提供的iOS项目,您将看到它们包括示例密钥。将私钥粘贴到ASN.1解码器中。您会注意到,您有一个SEQUENCE标记,后面跟着几个INTEGER值。

现在粘贴公钥。您会注意到公钥与私钥有两个共同点。模数和指数。在私钥中,这是第二个和第三个INTEGER值。它还具有一些顶部信息。它有2个额外的SEQUENCE,一个OBJECT ID,NULL和BIT STRING标记。

您还会注意到,在该项目中,他调用了一个特殊函数来处理该公钥。它做的就是剥离所有头信息,直到它到达最内层的SEQUENCE标记。此时,他像处理私钥一样处理它,并将其放入钥匙串中。

为什么要这样做?看一下标题和页脚文本。私钥说--- BEGIN RSA PRIVATE KEY ---,公钥说--- BEGIN PUBLIC KEY ---。您将在公钥中看到的对象ID是:1.2.840.113549.1.1.1。这是一个静态标记ID,用于识别所包含的密钥为RSA类型密钥。

由于私钥在前言中有RSA,因此假定它是一个RSA密钥,并且不需要头部ASN.1信息来标识密钥。公钥只是一个通用密钥,因此需要头部来确定密钥类型。
Keychain不会导入带有此ASN.1头的RSA密钥。您需要将其削减到最后的SEQUENCE。那时,您可以将其放入keychain,并且keychain能够推导出块大小和其他密钥属性。
因此,如果存在BEGIN RSA PRIVATE KEY,则无需执行削减操作。如果是-- BEGIN PRIVATE KEY---,则您需要在将其放入keychain之前削减这些初始标头。
在我的情况下,我还需要公钥。我们找不到从keychain获取它的方法(可能我们错过了什么),因此我们实际上从私钥创建了一个ASN.1公钥,然后将其导入到keycahin中。
在私钥(经过ASN.1头削减)中,您将看到一个SEQUENCE标记,后面跟着3个INTEGER标记(此后还有更多的INTEGERS,但我们只关心前三个)。
第一个是VERSION标记。第二个是模数,第三个是公共指数。
查看公钥(经过ASN.1头削减),您将看到SEQUENCE后面是2个INTEGERS。您猜对了,这是来自私钥的模数和公共指数。
因此,您需要做的全部是:
1. 从私钥中获取模数和公共指数 2. 在缓冲区中创建一个SEQUENCE标记,并将其长度设置为[模数长度]+ [指数长度]。(在将这些字节写入缓冲区时,您很可能需要颠倒字节的大小端。我至少这样做了。) 3. 添加您从私钥中获取的模数数据 4. 添加您从私钥中获取的指数数据
这就是从导入的私钥创建公钥所需做的全部。似乎没有太多关于导入在设备上不生成的RSA密钥的信息,我听说在设备上生成的密钥不包含这些ASN.1头文件,但我从未尝试过。我们的密钥非常大,需要设备花费太长时间才能生成。我能找到的唯一选择是使用OpenSSL,在其中必须为iOS编译自己的数字证书。在可能的情况下,我更愿意使用安全框架。
我还是比较新的iOS开发者,我相信有人知道一个简单的函数可以执行所有这些操作,但我找不到它,而且我已经寻找了。这似乎很好用,直到有一个更容易的API可用来处理密钥。
最后注意:项目中包含的私钥具有BIT STRING标记,但我从OpenSSL生成的私钥导入的私钥具有OCTET STRING标记。


0

我从MYcrypto库中找到了这段代码(BSD许可证)。它似乎可以实现你想要的功能。

    SecKeyRef importKey(NSData *data, 
                    SecExternalItemType type,
                    SecKeychainRef keychain,
                    SecKeyImportExportParameters *params) {
    SecExternalFormat inputFormat = (type==kSecItemTypeSessionKey) ?kSecFormatRawKey :kSecFormatUnknown;
    CFArrayRef items = NULL;

    params->version = SEC_KEY_IMPORT_EXPORT_PARAMS_VERSION;
    params->flags |= kSecKeyImportOnlyOne;
    params->keyAttributes |= CSSM_KEYATTR_EXTRACTABLE;
    if (keychain) {
        params->keyAttributes |= CSSM_KEYATTR_PERMANENT;
        if (type==kSecItemTypeSessionKey)
            params->keyUsage = CSSM_KEYUSE_ENCRYPT | CSSM_KEYUSE_DECRYPT;
        else if (type==kSecItemTypePublicKey)
            params->keyUsage = CSSM_KEYUSE_ENCRYPT | CSSM_KEYUSE_VERIFY | CSSM_KEYUSE_WRAP;
        else if (type==kSecItemTypePrivateKey)
            params->keyUsage = CSSM_KEYUSE_DECRYPT | CSSM_KEYUSE_SIGN;
    }
    if (!check(SecKeychainItemImport((CFDataRef)data, NULL, &inputFormat, &type,
                                     0, params, keychain, &items),
               @"SecKeychainItemImport"))
        return nil;
    if (!items || CFArrayGetCount(items) != 1)
        return nil;
    SecKeyRef key = (SecKeyRef)CFRetain(CFArrayGetValueAtIndex(items,0));
    CFRelease(items);
    return key; // caller must CFRelease
}

代码在iPhone SDK上并不能正常工作,尽管在实际电脑上进行一般的Mac OS X开发是可以的。不管怎样还是谢谢! - Anant
加入一些 #if TARGET_IPHONE_SIMULATOR#else 编译指令,你就可以得到在模拟器和设备上都能工作的东西。(你已经有了 Mac 版本,只需寻找 iPhone 的类比即可。我正在进行类似的任务,否则我会自己回答的。) - bbrown
@bbrown:兄弟,你一点也没帮上忙。 - Stefan Arentz
我将此投票否决是因为它实际上并没有做到@Anant想要的。如果您编辑它以包括iOS代码,它可能会成为一个好答案。 - user23743
@anisoptera:我已经成功将私钥导入到Mac应用程序的钥匙串中。现在,我想从钥匙串中导出私钥数据,而不让钥匙串要求密码。有什么方法可以实现这一点吗? - Subhash
1
我非常确定你不能这样做,因为允许随意的应用程序从钥匙串中提取私人数据而不经过用户允许会造成重大的安全漏洞。 - anisoptera

0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接