首页 文章

在android上解密php加密数据

提问于
浏览
2

Android 客户端(4.2.1)应用程序通过 HttpPost 请求将公钥发送到 PHP (5.6)API . 此API使用符合 AESRIJNDAEL_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 回答

  • 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 函数必须具有的样子:

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

相关问题