Add Custom Http Interceptor To Retrofit in Android For Encryption Communication

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.

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.

Code

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

Create SecureDataMessage data type for the encrypted message

data class SecureDataMessage (

    @SerializedName("message")
    val message : String

)

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.

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

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.


Comments

Leave a Reply

Your email address will not be published. Required fields are marked *