Bartłomiej Tomczak
Bartłomiej Tomczak

Reputation: 23

AES CBC in Android and PHP via Base64

So I have a specific problem in my code. It sometimes works, sometimes not and I have no idea why.

How it should work: Android encrypts a message using AES/CBC/PKCS5Padding with random IV and my secret key, converts encrypted message to base64 and sends it to server using POST method. Server converts message to binary form, decrypts it and appends smile to message. Next sends message back to Android. If message is empty, server sends me a "Empty" text.

How it works: I always receive data from server so connection is fine. Unfortunately I get 3 type of answers:

  1. My message with smile - that's OK
  2. "Empty..." text - but decrypt works somehow, no problem in PHP debug mode
  3. An error that my IV is too short - very rarely

A clue: I looked in base64 data and noticed that situation 2 appears when in base64 string is "+" char, but I don't know how it could help.

Android part to send data do server:

HttpURLConnection urlConnection;
String message = null;
String answer = null;
String data = "a piece of data";

try {
    byte[] wynikByte = encrypt(data.getBytes("UTF-8"));
    message = Base64.encodeToString(wynikByte, Base64.DEFAULT);
} catch (UnsupportedEncodingException ex){
    Log.e("CRYPT", "Not working");
}

try {
    // Connect to server
    urlConnection = (HttpURLConnection) ((new URL(url).openConnection()));
    urlConnection.setDoOutput(true);
    urlConnection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
    urlConnection.setRequestMethod("POST");
    urlConnection.connect();

    // Send to server
    OutputStream outputStream = urlConnection.getOutputStream();
    BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream, "UTF-8"));
    writer.write("dane=" + message);
    writer.close();
    outputStream.close();

    // Read answer
    BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(urlConnection.getInputStream(), "UTF-8"));
    String line = null;
    StringBuilder sb = new StringBuilder();
    while ((line = bufferedReader.readLine()) != null) {
        sb.append(line);
    }
    bufferedReader.close();
    answer = sb.toString();

} catch (UnsupportedEncodingException | IOException ex) {
        e.printStackTrace();
    }   
return message + "\n" + answer;

Android encrypt method:

public static byte[] encrypt(byte[] plaintext) {
    try {
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        SecretKey key = new SecretKeySpec(hexStringToByteArray(klucz2), "AES");

        SecureRandom random = new SecureRandom();
        byte iv[] = new byte[16];//generate random 16 byte IV AES is always 16bytes
        random.nextBytes(iv);
        IvParameterSpec ivspec = new IvParameterSpec(iv);

        cipher.init(Cipher.ENCRYPT_MODE, key, ivspec);
        byte[] encrypted = cipher.doFinal(plaintext);
        byte[] ciphertext = new byte[iv.length + encrypted.length];
        System.arraycopy(iv, 0, ciphertext, 0, iv.length);
        System.arraycopy(encrypted, 0, ciphertext, iv.length, encrypted.length);
        return ciphertext;

    } catch (InvalidKeyException | NoSuchAlgorithmException
            | NoSuchPaddingException
            | IllegalBlockSizeException | InvalidAlgorithmParameterException
            | BadPaddingException e) {
        throw new IllegalStateException(
                "CBC encryption with standard algorithm should never fail",
                e);
    }
} 

PHP file with my secret key also used in android app:

<?php
if (isset($_POST['dane']))
{
    $dane = $_POST['dane'];
    $key = pack('H*', "73f826a001837efe6278b82789267aca");

    $blocksize = mcrypt_get_block_size('rijndael_128', 'cbc');
    $ciphertext = base64_decode($dane, $powodzenie);
    $iv_size = mcrypt_get_iv_size(MCRYPT_RIJNDAEL_128, MCRYPT_MODE_CBC);
    $iv_old = substr($ciphertext, 0, $iv_size);
    $ciphertext = substr($ciphertext, $iv_size);
    $plaintext = mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $key, $ciphertext, MCRYPT_MODE_CBC, $iv_old);
    $plaintext  = pkcs5_unpad($plaintext);
    if($plaintext == "")
    {
        echo "Empty...";
        return;
    }
    $plaintext = $plaintext . " :)";
    echo $plaintext;
} else {
    echo "Dane is empty";
}

// PHP don't have pkcs5 methods to pad
function pkcs5_pad ($text, $blocksize) 
{ 
    $pad = $blocksize - (strlen($text) % $blocksize); 
    return $text . str_repeat(chr($pad), $pad); 
} 

// PHP don't have pkcs5 methods to unpad
function pkcs5_unpad($text) 
{ 
    $pad = ord($text{strlen($text)-1}); 
    if ($pad > strlen($text)) return false; 
    if (strspn($text, chr($pad), strlen($text) - $pad) != $pad) return false; 
    return substr($text, 0, -1 * $pad); 
} 
?>

Upvotes: 2

Views: 481

Answers (1)

Sammitch
Sammitch

Reputation: 32232

You need to properly encode the message string prior to the marked line. Yes, base64 is 7bit-safe, but it also contains characters that are significant in form-encoded data. [+ and = specifically]

// Send to server
OutputStream outputStream = urlConnection.getOutputStream();
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream, "UTF-8"));
writer.write("dane=" + message); // here
writer.close();
outputStream.close();

Solution 1 would be to replace + and = with %2B and %3D respectively.

Solution 2 would be to switch to multipart encoding.

My preference would be Solution 2. It's a bit more work to implement, but you get much more bang for your buck.

Upvotes: 1

Related Questions