RSA:在 iOS 中加密,在 Java 中解密
Posted
技术标签:
【中文标题】RSA:在 iOS 中加密,在 Java 中解密【英文标题】:RSA: encrypt in iOS, decrypt in Java 【发布时间】:2014-11-24 08:43:55 【问题描述】:我有一个从 Java 服务器发送的公钥。在我解码并去除 ASN.1 标头之前,base64 编码的字符串匹配。我使用SecItemAdd
将公钥存储在钥匙串中。
所以我尝试使用公钥加密数据并使用 Java 中的私钥对其进行解密。我在 ios 端使用 SecKeyEncrypt
,在 Java 端使用 Cipher
。
我要加密的是对我的实际数据进行加密的对称 AES 密钥,因此密钥长度为 16 个字节。当简单地对密钥进行 base64 编码时,一切正常,所以我知道这种 RSA 加密有问题。
这是我的 iOS 调用示例:
OSStatus sanityCheck = SecKeyEncrypt(publicKey,
kSecPaddingPKCS1,
(const uint8_t *) [incomingData bytes],
keyBufferSize,
cipherBuffer,
&cipherBufferSize
);
这是我的 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;
我尝试了很多组合,但没有任何效果。
iOS:PKCS1,Java:RSA/ECB/PKCS1Padding iOS:PKCS1,Java:RSA 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-256AndMGF1Padding
我也尝试过使用内部 Java 提供程序以及 BouncyCastle 提供程序。 javax.crypto.BadPaddingException
每次都会被抛出,但每个组合的消息都不同。有些显示Data must start with zero
,有些显示Message is larger than modulus
。
iOS: PKCS1, Java: RSA
不会引发异常,但得到的解密后的byte[]
数组的长度应该是 16,但它的长度是 256,这意味着填充没有被正确剥离。
有人可以帮忙吗?
*** 编辑 ***
当我进行更多测试时,我看到了这个页面 (http://javadoc.iaik.tugraz.at/iaik_jce/current/iaik/pkcs/pkcs1/RSACipher.html),它基本上告诉我 RSA == RSA/None/PKCS1Padding
。解密在没有例外的意义上是有效的,但我仍然得到一个解密的密钥,其 byte[] 的长度为 256 而不是长度为 16。
另一个兴趣点。看来,如果 Java 服务器具有从 iOS 设备生成并使用Cipher.getInstance("RSA")
加密的公钥,则手机能够使用 RSA/PKCS1 正确解码消息。
*** 编辑 2 ***
我已经查看了这些教程,并在 iOS 端再次查看了我的代码:
http://blog.flirble.org/2011/01/05/rsa-public-key-openssl-ios/ http://blog.wingsofhermes.org/?p=42 http://blog.wingsofhermes.org/?p=75据我所知,我的代码一切正常。一个显着的区别是我保存密钥的方式,所以我尝试以另一种方式保存它:
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);
保存成功,但最终结果是一样的:解密后的 AES 密钥仍然是 256 字节而不是 16 字节。
【问题讨论】:
【参考方案1】:我有同样的问题。可与kSecPaddingNone
一起使用,但不可与kSecPaddingPKCS1
与Java 代码中的任何PKCS1
组合一起使用。
但是,在没有填充的情况下使用它并不是一个好主意。
因此,在 iOS 上,将 kSecPaddingNone
替换为 kSecPaddingOAEP
并在您的 Java 代码中使用 RSA/NONE/OAEPWithSHA1AndMGF1Padding
。这对我有用。
【讨论】:
谢谢@robert-vojta!这也对我有用。对于那些有同样问题的人,你可以在我的答案上进行替换。我不想因为他的解决方案而受到赞扬! 谢谢罗伯特!这也确实解决了我的问题。【参考方案2】:
RSA/None/NoPadding
的解决方案
好的,我让它工作了,但是没有填充。这部分真的让我很沮丧,我把它留给其他人来帮忙。也许我最终会在 github 上发布我所拥有的库,一个用于 Obj-C,一个用于 Java。这是我目前发现的。
TL;DR:将密钥保存到具有最少属性的钥匙串中,以使检索更简单。使用SecKeyEncrypt
加密,但使用kSecPaddingNone
。使用 BouncyCastle 和算法 RSA/None/NoPadding
在 Java 端解密。
从 Java 向 iOS 发送 RSA 公钥
使用 X.509 证书
我想验证直接发送公钥、剥离 ASN.1 标头并保存是否真的在做它应该做的事情。所以我考虑将公钥作为证书发送过来。我想感谢 David Benko 提供的加密库 (https://github.com/DavidBenko/DBTransitEncryption) 帮助我完成了证书转换。我实际上并没有使用他的库,因为 1. 我已经在使用 RNCryptor
/JNCryptor
进行 AES 加密,并且 2. 他没有 Java 端组件,所以我需要编写自己的 AES 解密在那里,我不想那样做。对于那些感兴趣并希望采用这种方法的人,这是我在 Java 端创建证书然后将该证书转换为 iOS 上的公钥的代码:
* 重要提示:请将e.printStackTrace()
替换为真实的日志记录语句。我只将它用于测试,不用于生产。
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:
- (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;
使用 RSA 公钥
请务必注意,您不需要将公钥作为证书发送。事实上,在发现公钥保存不正确(见下文)后,我恢复了这段代码并将公钥保存到我的设备中。您需要删除其中一篇博文中提到的 ASN.1
标头。该代码在此处重新发布(为清晰起见进行了格式化)。
+ (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]);
所以我会像这样简单地保存密钥:
- (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"];
将 RSA 公钥保存到钥匙串
这太疯狂了。事实证明,即使我将钥匙保存到钥匙串中,我检索到的并不是我放入的!当我将保存的 base64 密钥与用于加密 AES 密钥的 base64 密钥进行比较时,我偶然发现了这一点。所以我发现最好简化保存密钥时使用的 NSDictionary。这是我最终得到的结果:
- (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];
要检索我的密钥,我使用以下方法:
- (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;
在一天结束时,我只能让它在没有填充的情况下工作。我不确定为什么BouncyCastle
无法删除填充,所以如果有人有任何见解,请告诉我。
这是我的加密代码(修改自 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;
这是我在 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;
【讨论】:
嗨 mikeho,我遇到了和你一样的问题。我看到您将使用 RSA 算法在 iOS 和 android 之间发布一个用于加密/解密数据的库。你做到了吗?以上是关于RSA:在 iOS 中加密,在 Java 中解密的主要内容,如果未能解决你的问题,请参考以下文章