Android
客户端(4.2.1)应用程序通过 HttpPost
请求将公钥发送到 PHP
(5.6)API . 此API使用符合 AES
的 RIJNDAEL_128
加密数据,然后使用具有OpenSSL公共加密和 RSA_PKCS1_OAEP_PADDING
的客户端公钥加密AES加密的密钥 . 它将通过 XML
编码的数据 base64
发送回客户端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
描述为
具有SHA-1,MGF1和空编码参数的PKCS#1 v2.0中定义的EME-OAEP .
在OpenSSL文档中找到here . 之后我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应用程序通过 BasicResponseHandler
通过 HttpPost
请求获取返回数据 . 此返回的XML字符串有效,并通过Simple解析为相应的java对象 . 在保存传输数据的实际内容的类中,我当前尝试解密数据 . 我使用转换_719616解密AES密钥,因为this site(只有我能找到)是一个有效的字符串,它似乎相当于我在PHP中使用的填充 . 我包含了生成私钥的方式,因为它与生成发送到PHP API的公钥的方式相同 . 这是 class :
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密码转换字符串组合 . 通过省略openssl_public_encrypt中的padding参数(默认为 OPENSSL_PKCS1_PADDING
)和Cipher转换字符串 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");
但是这并没有给出任何人类可读的文本,所以我认为关键是错误的或我改变它的方法 .
另外,在解密函数mcrypt_decrypt中需要解密PHP中的AES加密数据 . 正如您在代码中看到的那样,我发送它但是在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";
}
}
1 回答
您正在
doInBackground
中生成一个RSA密钥对,并告知主机使用该密钥对的公共一半来加密DEK(数据加密密钥) . 然后,您将在getData
中生成完全不同的RSA密钥对,并尝试使用该密钥对的私有一半来解密加密的DEK . 公钥加密的工作方式是使用密钥对的公共密钥进行加密,并使用 same keypair 的私有密码进行解密;公共和私人的一半是 mathematically related . 您需要至少保存并使用密钥对的私有一半(可选择两半的密钥对),其公共一半发送 .一旦你正确获得DEK,为了解密CBC模式数据, yes you do need to use the same IV for decryption 就像用于加密一样 . 您的接收器需要将其放入
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
函数必须具有的样子: