在android上解密php加密数据

时间:2014-09-26 21:16:55

标签: java php android security encryption

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描述为

  

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

找到here的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应用程序通过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";
    }
}

1 个答案:

答案 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;
}