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