简体   繁体   English

RSA:在iOS中加密,用Java解密

[英]RSA: encrypt in iOS, decrypt in Java

I have a public key that's sent from a Java server. 我有一个从Java服务器发送的公钥。 The base64 encoded strings match before I decode and strip out the ASN.1 headers. 在解码和去除ASN.1标头之前,base64编码的字符串匹配。 I store the public key in the keychain with SecItemAdd . 我使用SecItemAdd将公钥存储在钥匙串中。

So I'm trying to encrypt the data using the public key and decrypt it with the private key in Java. 所以我正在尝试使用公钥加密数据,并使用Java中的私钥对其进行解密。 I'm using SecKeyEncrypt on the iOS side and Cipher on the Java side. 我在iOS端使用SecKeyEncrypt ,在Java端使用Cipher

What I'm encrypting is the symmetric AES key that encrypts my actual data, so the key length is 16 bytes. 我正在加密的是加密我的实际数据的对称AES密钥,因此密钥长度为16个字节。 When simply base64 encoding the key, everything works, so I know something is wrong with this RSA encryption. 当简单地对base64进行编码时,一切正常,所以我知道这个RSA加密有问题。

Here's an example of my iOS call: 这是我的iOS调用示例:

OSStatus sanityCheck = SecKeyEncrypt(publicKey,
        kSecPaddingPKCS1,
        (const uint8_t *) [incomingData bytes],
        keyBufferSize,
        cipherBuffer,
        &cipherBufferSize
);

Here's an example of my Java call: 这是我的Java调用的一个例子:

public static byte[] decryptMessage (byte[] message, PrivateKey privateKey, String algorithm) {
    if (message == null || privateKey == null) {
        return null;
    }
    Cipher cipher = createCipher(Cipher.DECRYPT_MODE, privateKey, algorithm, false);
    if (cipher == null) {
        return null;
    }

    try {
        return cipher.doFinal(message);
    }
    catch (IllegalBlockSizeException e) {
        e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
        return null;
    }
    catch (BadPaddingException e) {
        e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
        return null;
    }
}

private static Cipher createCipher (int mode, Key encryptionKey, String algorithm, boolean useBouncyCastle) {
    Cipher cipher;

    try {
        if (useBouncyCastle) {
            Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
            cipher = Cipher.getInstance(algorithm, "BC");
        }
        else {
            cipher = Cipher.getInstance(algorithm);
        }
    }
    catch (NoSuchAlgorithmException e) {
        e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
        return null;
    }
    catch (NoSuchPaddingException e) {
        e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
        return null;
    }
    catch (NoSuchProviderException e) {
        e.printStackTrace();
        return null;
    }

    try {
        cipher.init(mode, encryptionKey);
    }
    catch (InvalidKeyException e) {
        e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
        return null;
    }

    return cipher;
}

I've tried so many combinations and nothing has worked. 我尝试了很多组合,但没有任何效果。

  • iOS: PKCS1, Java: RSA/ECB/PKCS1Padding iOS:PKCS1,Java:RSA / ECB / PKCS1Padding
  • iOS: PKCS1, Java: RSA iOS:PKCS1,Java:RSA
  • iOS: PKCS1, Java: RSA/None/PKCS1Padding (throws org.bouncycastle.crypto.DataLengthException: input too large for RSA cipher. ) iOS:PKCS1,Java:RSA / None / PKCS1Padding(抛出org.bouncycastle.crypto.DataLengthException: input too large for RSA cipher.
  • iOS: OAEP, Java: RSA/ECB/OAEPWithSHA-1AndMGF1Padding iOS:OAEP,Java:RSA / ECB / OAEPWithSHA-1AndMGF1Padding
  • iOS: OAEP, Java: RSA/ECB/OAEPWithSHA-256AndMGF1Padding iOS:OAEP,Java:RSA / ECB / OAEPWithSHA-256AndMGF1Padding

I've also tried using the internal Java provider as well as the BouncyCastle provider. 我也尝试使用内部Java提供程序以及BouncyCastle提供程序。 The javax.crypto.BadPaddingException gets thrown every time, but the message is different for each combination. 每次都会抛出javax.crypto.BadPaddingException ,但每个组合的消息都不同。 Some show Data must start with zero , while others are Message is larger than modulus . 一些节目Data must start with zero ,而其他节目则Message is larger than modulus

The iOS: PKCS1, Java: RSA doesn't throw an exception, but the resulting decrypted byte[] array should be length 16, but it's length 256, which means the padding isn't correctly stripped out. iOS: PKCS1, Java: RSA不会抛出异常,但生成的解密byte[]数组应该是长度为16,但它的长度为256,这意味着填充没有被正确剥离。

Can someone help? 有人可以帮忙吗?

*** EDIT *** *** 编辑 ***

As I'm doing more testing, I came across this page ( http://javadoc.iaik.tugraz.at/iaik_jce/current/iaik/pkcs/pkcs1/RSACipher.html ), which essentially tells me that RSA == RSA/None/PKCS1Padding . 当我进行更多测试时,我遇到了这个页面( http://javadoc.iaik.tugraz.at/iaik_jce/current/iaik/pkcs/pkcs1/RSACipher.html ),这实际上告诉我RSA == RSA/None/PKCS1Padding The decryption works in the sense that there are no exceptions, but I'm still getting a decrypted key whose byte[] is length 256 instead of length 16. 解密工作的意义是没有异常,但我仍然得到一个解密密钥,其byte []长度为256而不是长度为16。

Another point of interest. 另一个兴趣点。 It seems that if the Java server has the public key generated from the iOS device and encrypted using Cipher.getInstance("RSA") , the phone is able to decode the message correctly with RSA/PKCS1. 似乎如果Java服务器具有从iOS设备生成并使用Cipher.getInstance("RSA")加密的Cipher.getInstance("RSA") ,则电话能够使用RSA / PKCS1正确解码消息。

*** EDIT 2 *** *** 编辑2 ***

I have looked at these tutorials and looked through my code again on the iOS side: 我查看了这些教程,并在iOS端再次查看了我的代码:

As far as I can tell, my code is doing everything correctly. 据我所知,我的代码正在做正确的事情。 One significant difference was in how I was saving the key, so I tried saving it the other way: 一个显着的区别在于我如何保存密钥,所以我尝试以另一种方式保存它:

    OSStatus error = noErr;
    CFTypeRef persistPeer = NULL;

    NSMutableDictionary * keyAttr = [[NSMutableDictionary alloc] init];

    keyAttr[(__bridge id) kSecClass] = (__bridge id) kSecClassKey;
    keyAttr[(__bridge id) kSecAttrKeyType] = (__bridge id) kSecAttrKeyTypeRSA;
    keyAttr[(__bridge id) kSecAttrApplicationTag] = [secKeyWrapper getKeyTag:serverPublicKeyTag];
    keyAttr[(__bridge id) kSecValueData] = strippedServerPublicKey;
    keyAttr[(__bridge id) kSecReturnPersistentRef] = @YES;

    error = SecItemAdd((__bridge CFDictionaryRef) keyAttr, (CFTypeRef *)&persistPeer);

    if (persistPeer == nil || ( error != noErr && error != errSecDuplicateItem)) {
        NSLog(@"Problem adding public key to keychain");
        return;
    }

    CFRelease(persistPeer);

That save was successful, but the end result was the same: the decrypted AES key was still 256 bytes long instead of 16 bytes. 保存成功,但最终结果是相同的:解密的AES密钥仍然是256字节长而不是16字节。

I had same problem. 我有同样的问题。 Does work with kSecPaddingNone , but doesn't work with kSecPaddingPKCS1 with any PKCS1 combination in Java code. 适用于kSecPaddingNone ,但不适用于Java代码中任何PKCS1组合的kSecPaddingPKCS1

But, it's not good idea to use it without padding. 但是,没有填充使用它并不是一个好主意。

So, on iOS, replace kSecPaddingNone with kSecPaddingOAEP and use RSA/NONE/OAEPWithSHA1AndMGF1Padding in your Java code. 因此,在iOS,更换kSecPaddingNonekSecPaddingOAEP和使用RSA/NONE/OAEPWithSHA1AndMGF1Padding在Java代码中。 This does work for me. 这对我有用。

Solution with RSA/None/NoPadding 使用RSA/None/NoPadding解决方案

Okay, so I got it working but WITHOUT PADDING . 好的,所以我让它工作但没有PADDING This part is really frustrating me and I leave it up to others to try to help out. 这部分让我非常沮丧,我将其留给其他人试图帮助。 Maybe I'll eventually release what I have as a library on github, one for Obj-C, one for Java. 也许我最终将在github上发布我的库,一个用于Obj-C,一个用于Java。 Here's what I found out so far. 这是我到目前为止发现的。

TL;DR : save the key to the keychain with minimal attributes to make retrieval simpler. TL; DR :使用最少的属性将密钥保存到钥匙串,以使检索更简单。 Encrypt with SecKeyEncrypt but use kSecPaddingNone . 加密与SecKeyEncrypt但使用kSecPaddingNone Decrypt on Java side with BouncyCastle and algorithm RSA/None/NoPadding . 使用BouncyCastle和算法RSA/None/NoPadding解密Java端。

Sending RSA Public Key to iOS from Java 从Java向iOS发送RSA公钥

Using X.509 Certificate 使用X.509证书

I wanted to verify whether sending the public key directly, stripping out the ASN.1 header and saving was actually doing what it was supposed to do. 我想验证是否直接发送公钥,剥离ASN.1标头并保存实际上正在做它应该做的事情。 So I looked at sending the public key over as a certificate instead. 所以我看着将公钥作为证书发送。 I want to give credit to David Benko for providing an encryption library ( https://github.com/DavidBenko/DBTransitEncryption ) that helped me with the certificate conversion. 我想赞扬David Benko提供的加密库( https://github.com/DavidBenko/DBTransitEncryption )帮助我进行证书转换。 I didn't actually use his library because 1. I'm already using RNCryptor / JNCryptor for my AES encryption and 2. he doesn't have a Java side component, so I would need to write my own AES decryption there and I didn't want to do that. 我实际上并没有使用他的库,因为1.我已经使用RNCryptor / JNCryptor进行AES加密了2.他没有Java端组件,所以我需要在那里编写自己的AES解密而且我没有我不想这样做。 For those interested and want to take this approach, here's my code for creating a certificate on the Java side and then converting that certificate to a public key on iOS: 对于那些感兴趣并希望采用这种方法的人,这是我在Java端创建证书然后将该证书转换为iOS上的公钥的代码:

* Important Note: Please replace e.printStackTrace() with real logging statements. *重要说明:请将e.printStackTrace()替换为真实的日志记录语句。 I only used this for testing and NOT in production. 我只用它进行测试,而不是用于生产。

Java : Java

public static X509Certificate generateCertificate (KeyPair newKeys) {
    Security.addProvider(new BouncyCastleProvider());
    Date startDate = new Date();
    Date expiryDate = new DateTime().plusYears(100).toDate();

    BigInteger serialNumber = new BigInteger(10, new Random());
    try {
        ContentSigner sigGen = new JcaContentSignerBuilder("SHA1withRSA").setProvider("BC").build(newKeys
                                                                                                          .getPrivate());
        SubjectPublicKeyInfo subjectPublicKeyInfo = new SubjectPublicKeyInfo(ASN1Sequence.getInstance(newKeys
                                                                                                              .getPublic().getEncoded()
                                                                                                              ));
        X500Name dnName = new X500Name("CN=FoodJudge API Certificate");
        X509v1CertificateBuilder builder = new X509v1CertificateBuilder(dnName,
                                                                        serialNumber,
                                                                        startDate, expiryDate,
                                                                        dnName,
                                                                        subjectPublicKeyInfo);
        X509CertificateHolder holder = builder.build(sigGen);
        return new JcaX509CertificateConverter().setProvider("BC").getCertificate(holder);
    }
    catch (OperatorCreationException e) {
        e.printStackTrace();
    }
    catch (CertificateException e) {
        e.printStackTrace();
    }
    return null;
}

Obj-C : Obj-C

- (SecKeyRef)extractPublicKeyFromCertificate:(NSData *)certificateBytes {
    if (certificateBytes == nil) {
        return nil;
    }

    SecCertificateRef certificate = SecCertificateCreateWithData(kCFAllocatorDefault, ( __bridge CFDataRef) certificateBytes);
    if (certificate == nil) {
        NSLog(@"Can not read certificate from data");
        return false;
    }

    SecTrustRef trust;
    SecPolicyRef policy = SecPolicyCreateBasicX509();
    OSStatus returnCode = SecTrustCreateWithCertificates(certificate, policy, &trust);

    // release the certificate as we're done using it
    CFRelease(certificate);
    // release the policy
    CFRelease(policy);

    if (returnCode != errSecSuccess) {
        NSLog(@"SecTrustCreateWithCertificates fail. Error Code: %d", (int)returnCode);
        return nil;
    }

    SecTrustResultType trustResultType;
    returnCode = SecTrustEvaluate(trust, &trustResultType);
    if (returnCode != errSecSuccess) {
        // TODO log
        CFRelease(trust);
        return nil;
    }

    SecKeyRef publicKey = SecTrustCopyPublicKey(trust);
    CFRelease(trust);

    if (publicKey == nil) {
        NSLog(@"SecTrustCopyPublicKey fail");
        return nil;
    }

    return publicKey;
}

Using RSA Public Key 使用RSA公钥

It's important to note that you don't need send the public key over as a certificate. 请务必注意,您不需要将公钥作为证书发送。 In fact, after discovering that the public key was being saved incorrectly (see below), I reverted this code and saved the public key to my device. 实际上,在发现公钥被错误保存后(见下文),我还原了这段代码并将公钥保存到我的设备中。 You'll need to strip the ASN.1 header as mentioned in one of the blog posts. 您需要删除其中一篇博文中提到的ASN.1标头。 That code is reposted here (formatted for clarity). 该代码在此处重新发布(为清晰起见而格式化)。

+ (NSData *)stripPublicKeyHeader:(NSData *)keyBits {
    // Skip ASN.1 public key header
    if (keyBits == nil) {
        return nil;
    }

    unsigned int len = [keyBits length];
    if (!len) {
        return nil;
    }

    unsigned char *c_key = (unsigned char *)[keyBits bytes];
    unsigned int  idx    = 0;

    if (c_key[idx++] != 0x30) {
        return nil;
    }

    if (c_key[idx] > 0x80) {
        idx += c_key[idx] - 0x80 + 1;
    }
    else {
        idx++;
    }

    if (idx >= len) {
        return nil;
    }

    if (c_key[idx] != 0x30) {
        return nil;
    }

    idx += 15;

    if (idx >= len - 2) {
        return nil;
    }

    if (c_key[idx++] != 0x03) {
        return nil;
    }

    if (c_key[idx] > 0x80) {
        idx += c_key[idx] - 0x80 + 1;
    }
    else {
        idx++;
    }

    if (idx >= len) {
        return nil;
    }

    if (c_key[idx++] != 0x00) {
        return nil;
    }

    if (idx >= len) {
        return nil;
    }

    // Now make a new NSData from this buffer
    return([NSData dataWithBytes:&c_key[idx] length:len - idx]);
}

So I would simply save the key like so: 所以我会像这样保存密钥:

- (void)storeServerPublicKey:(NSString *)serverPublicKey {
    if (!serverPublicKey) {
        return;
    }
    SecKeyWrapper *secKeyWrapper = [SecKeyWrapper sharedWrapper];
    NSData *decryptedServerPublicKey = [[NSData alloc] initWithBase64EncodedString:serverPublicKey options:0];

    NSData *strippedServerPublicKey = [SecKeyWrapper stripPublicKeyHeader:decryptedServerPublicKey];
    if (!strippedServerPublicKey) {
        return;
    }
    [secKeyWrapper savePublicKeyToKeychain:strippedServerPublicKey tag:@"com.sampleapp.publickey"];
}

Saving RSA Public Key to Keychain 将RSA公钥保存到Keychain

It was maddening. 这令人抓狂。 It turned out that even though I saved my key to the keychain, what I retrieved wasn't what I put in! 事实证明,即使我将钥匙保存在钥匙链上,我检索的也不是我放入的钥匙! I found this out by accident when I was comparing the base64 key that I was saving to the base64 key that I was using to encrypt my AES key. 当我将保存的base64密钥与我用来加密AES密钥的base64密钥进行比较时,我意外地发现了这一点。 So I found out that it's better to simplify the NSDictionary used when saving the key. 所以我发现最好简化保存密钥时使用的NSDictionary。 Here's what I ended up with: 这是我最终得到的:

- (void)savePublicKeyToKeychain:(NSData *)key tag:(NSString *)tagString {
    NSData *tag = [self getKeyTag:tagString];

    NSDictionary *saveDict = @{
            (__bridge id) kSecClass : (__bridge id) kSecClassKey,
            (__bridge id) kSecAttrKeyType : (__bridge id) kSecAttrKeyTypeRSA,
            (__bridge id) kSecAttrApplicationTag : tag,
            (__bridge id) kSecAttrKeyClass : (__bridge id) kSecAttrKeyClassPublic,
            (__bridge id) kSecValueData : key
    };
    [self saveKeyToKeychain:saveDict tag:tagString];
}

- (void)saveKeyToKeychain:(NSDictionary *)saveDict tag:(NSString *)tagString {
    OSStatus sanityCheck = SecItemAdd((__bridge CFDictionaryRef) saveDict, NULL);
    if (sanityCheck != errSecSuccess) {
        if (sanityCheck == errSecDuplicateItem) {
            // delete the duplicate and save again
            sanityCheck = SecItemDelete((__bridge CFDictionaryRef) saveDict);
            sanityCheck = SecItemAdd((__bridge CFDictionaryRef) saveDict, NULL);
        }
        if (sanityCheck != errSecSuccess) {
            NSLog(@"Problem saving the key to keychain, OSStatus == %d.", (int) sanityCheck);
        }
    }
    // remove from cache
    [keyCache removeObjectForKey:tagString];
}

To retrieve my key, I use the following methods: 要检索我的密钥,我使用以下方法:

 - (SecKeyRef)getKeyRef:(NSString *)tagString isPrivate:(BOOL)isPrivate {
     NSData *tag = [self getKeyTag:tagString];

     id keyClass = (__bridge id) kSecAttrKeyClassPublic;
     if (isPrivate) {
         keyClass = (__bridge id) kSecAttrKeyClassPrivate;
     }

     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
     };
     return [self getKeyRef:queryDict tag:tagString];
 }

- (SecKeyRef)getKeyRef:(NSDictionary *)query tag:(NSString *)tagString {
    SecKeyRef keyReference = NULL;
    OSStatus sanityCheck = SecItemCopyMatching((__bridge CFDictionaryRef) query, (CFTypeRef *) &keyReference);
    if (sanityCheck != errSecSuccess) {
        NSLog(@"Error trying to retrieve key from keychain. tag: %@. sanityCheck: %li", tagString, sanityCheck);
        return nil;
    }
    return keyReference;
}

At the end of the day, I was able to only get it working without padding. 在一天结束时,我只能在没有填充的情况下使其工作。 I'm not sure why BouncyCastle couldn't remove the padding, so if anyone has any insight, let me know. 我不确定为什么BouncyCastle无法删除填充,所以如果有人有任何见解,请告诉我。

Here's my code for encrypting (modified from David Benko ): 这是我的加密代码(由David Benko修改):

- (NSData *)encryptData:(NSData *)content usingPublicKey:(NSString *)publicKeyTag {
    SecKeyRef publicKey = [self getKeyRef:publicKeyTag isPrivate:NO];
    NSData *keyBits = [self getKeyBitsFromKey:publicKey];
    NSString *keyString = [keyBits base64EncodedStringWithOptions:0];
    NSAssert(publicKey != nil,@"Public key can not be nil");

    size_t cipherLen = SecKeyGetBlockSize(publicKey); // convert to byte
    void *cipher = malloc(cipherLen);
    size_t maxPlainLen = cipherLen - 12;

    size_t plainLen = [content length];
    if (plainLen > maxPlainLen) {
        NSLog(@"content(%ld) is too long, must < %ld", plainLen, maxPlainLen);
        return nil;
    }

    void *plain = malloc(plainLen);
    [content getBytes:plain
               length:plainLen];

    OSStatus returnCode = SecKeyEncrypt(publicKey, kSecPaddingNone, plain,
            plainLen, cipher, &cipherLen);

    NSData *result = nil;
    if (returnCode != errSecSuccess) {
        NSLog(@"SecKeyEncrypt fail. Error Code: %d", (int)returnCode);
    }
    else {
        result = [NSData dataWithBytes:cipher
                                length:cipherLen];
    }

    free(plain);
    free(cipher);

    return result;
}

Here's how I decrypt on the Java side: 这是我在Java方面解密的方式:

private Response authenticate (String encryptedSymmetricString) {
    byte[] encryptedSymmetricKey = Base64.decodeBase64(encryptedSymmetricKeyString);
    String privateKey = Server.getServerPrivateKey();
    byte[] decryptedSymmetricKey = KeyHandler.decryptMessage(encryptedSymmetricKey, privateKey,
                                                             KeyHandler.ASYMMETRIC_CIPHER_ALGORITHM);
}

public static byte[] decryptMessage (byte[] message, String privateKeyString, String algorithm) {
    if (message == null || privateKeyString == null) {
        return null;
    }
    PrivateKey privateKey = getPrivateKey(privateKeyString);
    return decryptMessage(message, privateKey, algorithm);
}

public static byte[] decryptMessage (byte[] message, PrivateKey privateKey, String algorithm) {
    if (message == null || privateKey == null) {
        return null;
    }
    Cipher cipher = createCipher(Cipher.DECRYPT_MODE, privateKey, algorithm, true);
    if (cipher == null) {
        return null;
    }

    try {
        return cipher.doFinal(message);
    }
    catch (IllegalBlockSizeException e) {
        e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
        return null;
    }
    catch (BadPaddingException e) {
        e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
        return null;
    }
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM