在android上解密php加密数据

Android客户端(4.2.1)应用程序通过HttpPost请求将公钥发送到PHP (5.6)API。 此API使用符合AES RIJNDAEL_128对数据进行AES ,然后使用具有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)] -> base64[AES(data)], base64[RSA(AES-key)] -> 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编码。 阅读源代码( PHP OpenSSL实现的源代码)这相当于描述为RSA_PKCS1_OAEP_PADDING

在PKCS#1 v2.0中定义的EME-OAEP,具有SHA-1,MGF1和空编码参数。

在这里找到的OpenSSL文档中。 之后我对数据进行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对象。 在保存传输数据的实际内容的类中,我当前尝试解密数据。 我使用转换RSA/ECB/OAEPWithSHA-1AndMGF1Padding解密AES密钥,由于这个站点 (我只能找到)是一个有效的字符串,似乎相当于我在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); 

几乎所有PHP openssl_public_encrypt填充参数的Bad padding exceptions – 我试过的Android Cipher转换字符串组合。 通过省略openssl_public_encrypt中的padding参数(默认为OPENSSL_PKCS1_PADDING和Cipher转换字符串Cipher.getInstance("RSA")使用标准的PHP填充参数,我没有得到错误的填充exception。 但是加密密钥似乎没有效果,因为AES解密失败了

 java.security.InvalidKeyException: Key length not 128/192/256 bits. 

我尝试使用固定密钥validation它(请参阅上面的PHP代码中的代码注释),并且在解密并将其转换为字符串后,我没有得到相同的密钥。 如果我正确读取Eclipse ADT调试器,它似乎只是乱码数据,虽然它是256位长。

可能是正确的Cipher转换字符串,用作PHP的OPENSSL_PKCS1_OAEP_PADDING的等效字符串。 阅读本文档我需要"algorithm/mode/padding"forms的转换字符串,我猜算算法= RSA但是我无法找到如何将OpenSSL(上面)文档中有关填充的内容转换为有效的密码转换字符串。 即什么是mode ,例如? 不幸的是,这个Android RSA解密(失败)/服务器端加密(openssl_public_encrypt)接受的答案并没有解决我的问题。

无论如何,这可能解决了我的问题,还是我的问题来自其他地方?

我该如何进一步调试呢? 将base64解码后的解密密钥转换为人类可读forms的正确方法是什么,以便将其与用于加密的密钥进行比较? 我尝试过:

 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 nameValuePairs = new ArrayList(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"; } } 

您正在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; }