Http Interceptor For Retrofit in Android To Encrypt Communication
In this post I show you how I implemented a custom Retrofit Http Interceptor for encryption in Android to communicate with a PHP server.

In this post I will show you how I added a custom Http Interceptor for Retrofit in Android.

For my use case, I wanted to setup an encryption method for every query. Instead of creating it for each query, which could be cumbersome when things evolve, I need to create a method to capture the query and edit it. I am using this for encryption but the core setup could be used for other situations.

The way I found to do it was to use Http Interceptors in Retrofit.

It was quite easy once you look at some of the code. I analyzed the code in

com.squareup.okhttp3:logging-interceptor:4.11.0

to view what it did with each parameter and how to build my own.

Retrofit HTTPInterceptor For Android

Code

RetrofitHelper

You need to add this to your Retrofit object class

val encryptionInterceptor: EncryptionInterceptor = EncryptionInterceptor(applicationContext)

        val okHttpClient = OkHttpClient.Builder()
            .addInterceptor(encryptionInterceptor)
            .addInterceptor(httpLoggingInterceptor)
            .build()
        

Like this

object RetrofitHelper {
    val baseUrl = "http://<server_address>/"
    fun getInstance(applicationContext : Context): Retrofit {
        val httpLoggingInterceptor: HttpLoggingInterceptor = HttpLoggingInterceptor()
        httpLoggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY)

        val encryptionInterceptor: EncryptionInterceptor = EncryptionInterceptor(applicationContext)

        val okHttpClient = OkHttpClient.Builder()
            .addInterceptor(encryptionInterceptor)
            .addInterceptor(httpLoggingInterceptor)
            .build()

        val gson = GsonBuilder()
            .setLenient().create()

        return Retrofit.Builder()
            .baseUrl(baseUrl)
            .addConverterFactory(nullOnEmptyConverterFactory)
            .addConverterFactory(GsonConverterFactory.create(gson))
            .client(okHttpClient)
            .build()
    }
}

val nullOnEmptyConverterFactory = object : Converter.Factory() {
    fun converterFactory() = this
    override fun responseBodyConverter(type: Type, annotations: Array<out Annotation>, retrofit: Retrofit) = object : Converter<ResponseBody, Any?> {
        val nextResponseBodyConverter = retrofit.nextResponseBodyConverter<Any?>(converterFactory(), type, annotations)
        override fun convert(value: ResponseBody) = if (value.contentLength() != 0L) nextResponseBodyConverter.convert(value) else null
    }
}

SecureDataMessage

This is specific to my situation due to my encryption needs.

Create SecureDataMessage data type for the encrypted message

data class SecureDataMessage (

    @SerializedName("message")
    val message : String

)

EncryptionInterceptor

This is again specific to my encryption need. You could edit this to fit your own situation or need.

Then create the EncryptionInterceptor class

class EncryptionInterceptor(val applicationContext : Context) : Interceptor {

    private val TAG : String = EncryptionInterceptor::class.java.simpleName

    override fun intercept(chain: Interceptor.Chain): Response {

        var request: Request = chain.request()
        var requestBody = request.body

        if (requestBody != null) {

            val buffer = Buffer()
            val mediaType: MediaType? = "application/x-www-form-urlencoded;charset=UTF-8".toMediaTypeOrNull() //text/plain; charset=utf-8
            requestBody.writeTo(buffer)

            val contentType = requestBody.contentType()
            val charset: Charset = contentType?.charset(UTF_8) ?: UTF_8
            val strToEncrypt = buffer.readString(charset)
            Log.d(TAG, strToEncrypt)
            val secureMsg : String?
            if (strToEncrypt.isNotEmpty()) {
                secureMsg = processEncryptData(strToEncrypt)
            } else {
            //needed to add parameter to validate connection on server side
                secureMsg = processEncryptData("{\"id\":500}")
            }
            if (secureMsg != null) {
                Log.d(TAG, secureMsg)
                val formattedStr = SecureDataMessage(secureMsg)
                val jsonObject = Gson().toJson(formattedStr)
                Log.d(TAG, jsonObject)
                val newRequestBody = RequestBody.create(mediaType, jsonObject)
                request = request.newBuilder().url(request.url).headers(request.headers).method(request.method, newRequestBody).build()
            }
        }
        Log.d(TAG, request.method)
        return chain.proceed(request)
    }

    private fun processEncryptData(str : String) : String? {
        val inputStream: InputStream = applicationContext.resources.openRawResource(R.raw.pk)
        val bytes: ByteArray = inputStream.readBytes()
        val pk = String(bytes)
        Log.d("Public Key", pk)
        val encryptedData = SecureMessage.encryptData(str, pk)
        if (encryptedData != null) {
            Log.d("Encrypted Data", encryptedData)
            return encryptedData
        }
        return null
    }
}

Let me explain a bit about what it does.

  • It reads the requestBody into a buffer.
  • Then it validates if the requestBody has anything in it, ex if there is a POST method with parameters.
  • if the requestBody is empty
    • it creates a simple serialized string with an id value due to my specific need. This value is processed on the server end later. Then it is encrypted.
    • if the requestBody is not empty, the request parameters are encrypted.
  • the encrypted parameters are put into a data type of SecureDataMessage I created.
  • This data type with the encrypted message is then serialized to json using Gson.
  • Finally the new request is built and returned.

PHP

Now a snippet of the server side code in PHP. This sample is a full script sample with intentions to validate connection. This scenario would be if you have no parameters to sent to server but still needed to validate connection with encryption. This is why the {"id":500} parameter is used.

This was thrown together in a test environment to validate implementation. Please do other checks in script to ensure your environment is safe.

<?php

require_once 'db_connect.php';

$con = $database->getConnection();

$data = json_decode(file_get_contents('php://input'));

$test_pk = "-----BEGIN PRIVATE KEY-----
....
this is just for testing, please carefully consider key placement
....
-----END PRIVATE KEY-----";

//looks for message from SecureDataMessage data type in Android
$message = $data->{'message'};

$dataArr = explode(":", $message);

$str = "";

for($i = 0; $i < count($dataArr); $i++){

    openssl_private_decrypt(base64_decode($dataArr[$i]), $test_decrypted, $test_pk);

    $str .= $test_decrypted;

}

$str = mb_convert_encoding($str, "UTF-8");

$json = json_decode($str, true);

//this use case was no actual parameters are used in rest of script, this script just searches database for all users. A parameter needed to be sent so I created an arbituary implementation using the {"id":500} - this checks if the id is not 500, then quit
if($json['id'] != 500){
    echo json_encode(array(
        array(
            'id' => "-1",
            )
        )
    );
    exit;
}

if($con != null){

        // use the connection here
    $sth = $con->query('SELECT * FROM people');

    // fetch all rows
    $rows = $sth->fetchAll();

    $responses = array();
    
    foreach($rows as $row) {
        $responses[] = $row;
    }
    echo json_encode($responses);
    $con = null;
} else {
//if database connection was unsuccessful, then send response
    echo json_encode(array(
                        array(
                            'id' => "-1",
                            )
                        )
                    );
}

?>

Note

The order you add the Http Interceptor in the RetrofitHelper makes a difference. Some cases it may matter, some not. In this case I needed to ensure all the visible data parameters sent are encrypted.