decrypting php encrypted data on android

2019-02-04 03:12发布

问题:

An Android client (4.2.1) application sends a public key via a HttpPost request to a PHP (5.6) API. This API encrypts the data with AES compliant RIJNDAEL_128, then encrypts the key for the AES encryption with the client public key with OpenSSL public encryption and RSA_PKCS1_OAEP_PADDING. It sends this data base64 encoded via XML back to the client android application which shall encrypt the data. I've setup a basic PHP test script which tests the whole process, this works as expected.

Currently I'm working on implementing the decryption in the client Android application but already decrypting the AES-key fails. I have other questions besides this current problem (see at the end).

Here is a text graphical synopsis of what is happening:

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

I'm encrypting the data with MCRYPT_RIJNDAEL_128 which I read is AES compatible (see PHP doc for mycrypt). Here is the code:

<?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);

The AES-key coming out of this is encoded with openssl_public_encrypt and the padding setting OPENSSL_PKCS1_OAEP_PADDING. Reading the source code (source of PHP OpenSSL implementation) this is equivalent to RSA_PKCS1_OAEP_PADDING described as

EME-OAEP as defined in PKCS #1 v2.0 with SHA-1, MGF1 and an empty encoding parameter.

in the OpenSSL documentation found here. Afterwards I base64_encode the data to be able to transfer it via an XML string to the client. The code looks like this:

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

The client Android application gets the return data via HttpPost request via a BasicResponseHandler. This returned XML string is valid and parsed via Simple to respective java objects. In the the class holding the actual content of the transferred data I currently try to decrypt the data. I decrypt the AES-key with the transformation RSA/ECB/OAEPWithSHA-1AndMGF1Padding which due to this site (only I could find) is a valid string and seems to be the equivalent of the padding I used in PHP. I included the way I generated the private key as it is the same way I generate the public key that was send to the PHP API. Here is that 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;
    }
}

Doing this I currently fail at line

byte[] key = cipherRSA.doFinal(cryptionKeyCrypted);

with Bad padding exceptions for nearly all PHP openssl_public_encrypt padding parameter - Android Cipher transformation string combinations I tried. Using the standard PHP padding parameter by omitting the padding parameter in the openssl_public_encrypt which defaults to OPENSSL_PKCS1_PADDING and a Cipher transformation string of just Cipher.getInstance("RSA") I do not get a bad padding exception. But the encrypted key seems not to be valid as AES decryption fails with

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

I tried validating this with a fixed key (see code comment in PHP code above) and I don't get the same key back after decrypting it and transforming it to a string. It seems it is just garbled data although it is 256 bits long if I read the Eclipse ADT debugger correctly.

What might be the correct Cipher transformation string to use as an equivalent for PHP's OPENSSL_PKCS1_OAEP_PADDING. Reading this documentation I need the transformation string in the form "algorithm/mode/padding", I guessed that algorithm = RSA but I couldn't find out how to translate what the OpenSSL (above) documentation states about the padding into a valid cipher transformation string. I.e. what is mode for example? Unfortunately this Android RSA decryption (fails) / server-side encryption (openssl_public_encrypt) accepted answer did not solve my problem.

Anyway might this solve my problem or does my problem originate elsewhere?

How would I further debug this? What is the correct way to transform the base64 decoded, decrypted key into a human readable form so I can compare it with the key used to encrypt? I tried with:

String keyString =  new String(keyBytes, "UTF-8");

But this doesn't give any human readable text back so I assume either the key is wrong or my method of transforming it.

Also decrypting the AES encrypted data in PHP the IV is needed in the decryption function mcrypt_decrypt. As you can see in the code I send it but it seems in Android this is not needed? Why so?

PS: I hope I provided all needed information, I can add further in the comments.

PPS: For completeness here is the Android client code making the HttpPost request:

@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:

You are generating one RSA keypair in doInBackground and telling the host to use the public half of that keypair to encrypt the DEK (data encryption key). You are then generating a completely different RSA keypair in getData and attempting to use the private half of that keypair to decrypt the encrypted DEK. The way public-key encryption works is you encrypt with the public half of a keypair and decrypt with the private half of the same keypair; the public and private halves are mathematically related. You need to save and use at least the private half of the keypair (optionally the keypair with both halves) whose public half you send.

Once you've got the DEK correctly, in order to decrypt CBC-mode data, yes you do need to use the same IV for decryption as was used for encryption. Your receiver needs to put it in an IvParameterSpec and pass that on the Cipher.init(direction,key[,params]) call. Alternatively if you can change the PHP, since you are using a new DEK for each message it is safe to use a fixed IV; easiest is to encrypt with '\0'x16 and allow the Java decrypt to default to all-zero.


Additionally you need to set Base64.decode with the parameter Base64.NO_WRAPas PHP will just put out the base64 delimited by \0. And to that you will also need to use the "AES/CBC/ZeroBytePadding" transformation cipher to decrypt the AES data as the PHP function mycrypt_encrypt will pad the data with zeros. Here is what the getData function will have to look like:

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