Android
客户端(4.2.1)应用程序通过HttpPost
请求向PHP
(5.6)API发送公钥。此API使用AES
兼容RIJNDAEL_128
对数据进行加密,然后使用OpenSSL公共加密和RSA_PKCS1_OAEP_PADDING
的客户端公钥加密AES加密密钥。它将通过base64
编码的数据XML
发送回客户端android应用程序,该应用程序将加密数据。我已经设置了一个基本的PHP测试脚本来测试整个过程,这可以按预期工作。
目前,我正致力于在客户端Android应用程序中实现解密,但已经解密AES密钥失败。除了当前的问题,我还有其他问题(见最后)。
以下是正在发生的事情的文字图形概要:
client -> public key -> API -> data -> AESencrypt(data), RSAencrypt(AES-key) -> base64encode[AES(data)], base64encode[RSA(AES-key)] -> <xml>base64[AES(data)], base64[RSA(AES-key)]</xml> -> client -> base64[AES(data)], base64[RSA(AES-key)] -> base64decode[AES(data)], base64decode[RSA(AES-key)] -> AESdecrypt(data), RSAdecrypt(AES-key) -> data
我使用MCRYPT_RIJNDAEL_128
加密数据,我读取的数据与AES兼容(请参阅PHP doc for mycrypt)。
这是代码:
<?php
$randomBytes = openssl_random_pseudo_bytes(32, $safe);
$randomKey = bin2hex($randomBytes);
$randomKeyPacked = pack('H*', $randomKey);
// test with fixed key:
// $randomKeyPacked = "12345678901234567890123456789012";
$iv_size = mcrypt_get_iv_size(MCRYPT_RIJNDAEL_128, MCRYPT_MODE_CBC);
$iv = mcrypt_create_iv($iv_size, MCRYPT_RAND);
$dataCrypted = mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $randomKeyPacked, $data, MCRYPT_MODE_CBC, $iv);
来自此的AES密钥使用openssl_public_encrypt和填充设置OPENSSL_PKCS1_OAEP_PADDING
进行编码。阅读源代码(source of PHP OpenSSL implementation),这相当于RSA_PKCS1_OAEP_PADDING
描述为
找到here的OpenSSL文档中的PKCS#1 v2.0中定义的EME-OAEP,包含SHA-1,MGF1和空编码参数。
。之后我base64_encode数据能够通过XML字符串将其传输到客户端。代码如下所示:
openssl_public_encrypt($randomKeyPacked, $cryptionKeyCrypted, $clientPublicKey, OPENSSL_PKCS1_OAEP_PADDING);
$content = array(
'cryptionKeyCryptedBase64' => base64_encode($cryptionKeyCrypted),
'cryptionIVBase64' => base64_encode($iv),
'dataCryptedBase64' => base64_encode($dataCrypted)
);
// $content gets parsed to a valid xml element here
客户端Android应用程序通过HttpPost
通过BasicResponseHandler
请求获取返回数据。此返回的XML字符串有效,并通过Simple解析为相应的java对象。在保存传输数据的实际内容的类中,我当前尝试解密数据。我使用转换RSA/ECB/OAEPWithSHA-1AndMGF1Padding
解密AES密钥,因为this site(我只能找到)是一个有效的字符串,它似乎相当于我在PHP中使用的填充。我包含了生成私钥的方式,因为它与生成发送到PHP API的公钥的方式相同。这是班级:
public class Content {
@Element
private String cryptionKeyCryptedBase64;
@Element
private String cryptionIVBase64;
@Element
private String dataCryptedBase64;
@SuppressLint("TrulyRandom")
public String getData() {
String dataDecrypted = null;
try {
PRNGFixes.apply(); // fix TrulyRandom
KeyPairGenerator keygen = KeyPairGenerator.getInstance("RSA");
keygen.initialize(2048);
KeyPair keypair = keygen.generateKeyPair();
PrivateKey privateKey = keypair.getPrivate();
byte[] cryptionKeyCrypted = Base64.decode(cryptionKeyCryptedBase64, Base64.DEFAULT);
//byte[] cryptionIV = Base64.decode(cryptionIVBase64, Base64.DEFAULT);
Cipher cipherRSA = Cipher.getInstance("RSA/ECB/OAEPWithSHA-1AndMGF1Padding");
cipherRSA.init(Cipher.DECRYPT_MODE, privateKey);
byte[] key = cipherRSA.doFinal(cryptionKeyCrypted);
byte[] dataCrytped = Base64.decode(dataCryptedBase64, Base64.DEFAULT);
SecretKeySpec skeySpec = new SecretKeySpec(key, "AES");
Cipher cipherAES = Cipher.getInstance("AES");
cipherAES.init(Cipher.DECRYPT_MODE, skeySpec);
byte[] decryptedAESBytes = cipherAES.doFinal(dataCrytped);
dataDecrypted = new String(decryptedAESBytes, "UTF-8");
} catch (Exception e) {
e.printStackTrace();
}
return dataDecrypted;
}
}
这样做我目前在第
行失败了byte[] key = cipherRSA.doFinal(cryptionKeyCrypted);
Bad padding exceptions
用于几乎所有PHP openssl_public_encrypt
填充参数 - 我试过的Android Cipher转换字符串组合。通过省略openssl_public_encrypt中默认为OPENSSL_PKCS1_PADDING
的填充参数和仅Cipher.getInstance("RSA")
的密码转换字符串,使用标准PHP填充参数,我没有得到错误的填充异常。但加密密钥似乎无效,因为AES解密失败
java.security.InvalidKeyException: Key length not 128/192/256 bits.
我尝试使用固定密钥对此进行验证(请参阅上面的PHP代码中的代码注释),并且在解密并将其转换为字符串后,我不会获得相同的密钥。如果我正确读取Eclipse ADT调试器,它似乎只是乱码数据,虽然它是256位长。
可能是正确的Cipher转换字符串,可用作PHP OPENSSL_PKCS1_OAEP_PADDING
的等效字符串。阅读this documentation我需要"algorithm/mode/padding"
形式的转换字符串,我猜算算法= RSA但我无法找到如何将OpenSSL(上述)文档中有关填充的内容转换为有效的密码转换字符串。即例如mode
是什么?
不幸的是,这个Android RSA decryption (fails) / server-side encryption (openssl_public_encrypt)接受的答案并没有解决我的问题。
无论如何,这可能会解决我的问题,还是我的问题来自其他地方?
我该如何进一步调试?将base64解码后的解密密钥转换为人类可读形式的正确方法是什么,以便将其与用于加密的密钥进行比较? 我尝试过:
String keyString = new String(keyBytes, "UTF-8");
但是这并没有给出任何人类可读的文本,所以我认为关键是错误的或我的方法是转换它。
在PHP中解密AES加密数据时,解密函数mcrypt_decrypt中需要IV。正如您在代码中看到的那样,我发送它但是在Android中似乎不需要它?为什么这样?
PS:我希望我提供了所有需要的信息,我可以在评论中进一步补充。
PPS:为了完整性,这里是制作HttpPost请求的Android客户端代码:
@SuppressLint("TrulyRandom")
protected String doInBackground(URI... urls) {
try {
System.setProperty("jsse.enableSNIExtension", "false");
HttpClient httpClient = createHttpClient();
HttpPost httpPost = new HttpPost(urls[0]);
PRNGFixes.apply(); // fix TrulyRandom
KeyPairGenerator keygen = KeyPairGenerator.getInstance("RSA");
keygen.initialize(2048);
KeyPair keypair = keygen.generateKeyPair();
PublicKey publickey = keypair.getPublic();
byte[] publicKeyBytes = publickey.getEncoded();
String pubkeystr = "-----BEGIN PUBLIC KEY-----\n"+Base64.encodeToString(publicKeyBytes,
Base64.DEFAULT)+"-----END PUBLIC KEY-----";
List<NameValuePair> nameValuePairs = new ArrayList<NameValuePair>(2);
nameValuePairs.add(new BasicNameValuePair("publickey", pubkeystr));
httpPost.setEntity(new UrlEncodedFormEntity(nameValuePairs));
// Execute HTTP Post Request
HttpResponse response = httpClient.execute(httpPost);
return new BasicResponseHandler().handleResponse(response);
} catch (Exception e) {
Toast toast = Toast.makeText(asyncResult.getContext(),
"unknown exception occured: " + e.getMessage(),
Toast.LENGTH_SHORT);
toast.show();
return "error";
}
}
答案 0 :(得分:1)
您正在doInBackground
中生成一个RSA密钥对,并告知主机使用该密钥对的公共一半来加密DEK(数据加密密钥)。然后,您在getData
中生成完全不同的RSA密钥对,并尝试使用该密钥对的私有一半来解密加密的DEK。公钥加密的工作方式是使用密钥对的公共密钥进行加密,并使用相同密钥对的私有部分进行解密;公共和私人部分在数学上相关。您需要至少保存并使用密钥对的私有部分(可选择两半的密钥对),其中包含您发送的公共部分。
正确获得DEK后,为了解密CBC模式数据,是,您需要使用与加密相同的IV进行解密。您的接收方需要将其放入IvParameterSpec
并在Cipher.init(direction,key[,params])
电话上传递。或者,如果您可以更改PHP,因为您为每条消息使用新的DEK,所以使用固定的IV是安全的;最简单的方法是使用'\0'x16
进行加密,并允许Java解密默认为全零。
此外,您需要使用参数Base64.NO_WRAP
设置Base64.decode,因为PHP只会输出由\0
分隔的base64。为此,您还需要使用"AES/CBC/ZeroBytePadding"
转换密码来解密AES数据,因为PHP函数mycrypt_encrypt
将使用零填充数据。
以下是getData
函数的含义:
public String getData() {
String dataDecrypted = null;
try {
byte[] cryptionKeyCrypted = Base64.decode(cryptionKeyCryptedBase64, Base64.NO_WRAP);
byte[] cryptionIV = Base64.decode(cryptionIVBase64, Base64.NO_WRAP);
Cipher cipherRSA = Cipher.getInstance("RSA/ECB/OAEPWithSHA-1AndMGF1Padding");
// get private key from the pair used to grab the public key to send to the api
cipherRSA.init(Cipher.DECRYPT_MODE, rsaKeyPair.getPrivateKey());
byte[] key = cipherRSA.doFinal(cryptionKeyCrypted);
byte[] dataCrytped = Base64.decode(dataCryptedBase64, Base64.NO_WRAP);
IvParameterSpec ivSpec = new IvParameterSpec(cryptionIV);
SecretKeySpec skeySpec = new SecretKeySpec(key, "AES");
Cipher cipherAES = Cipher.getInstance("AES/CBC/ZeroBytePadding");
cipherAES.init(Cipher.DECRYPT_MODE, skeySpec, ivSpec);
byte[] decryptedAESBytes = cipherAES.doFinal(dataCrytped);
dataDecrypted = new String(decryptedAESBytes, "UTF-8");
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return dataDecrypted;
}