Search code examples
androidkotlinkotlin-coroutinesktorktor-client

How to Decrypt and Use Encrypted Response in Ktor HTTP Client


I'm working on an Android application where I need to encrypt the request body before sending it to the server and then decrypt the response received from the server. I'm using Ktor's HTTP client for network requests and I have a CryptoUtils class that handles encryption and decryption using AES. Here's an outline of what I have done:

  1. Encryption: I have successfully installed a plugin to encrypt the request body before it's sent to the server. Here's the code for the encryption install (this exemple is for login only, i willl make it global later):
private fun HttpClientConfig<OkHttpConfig>.encryptionInstall() {
    install("EncryptRequest") {
        requestPipeline.intercept(HttpRequestPipeline.Transform) { request ->
            if (request !is EmptyContent) {
                val request = request as LoginDTO
                val originalBody = Json.encodeToString(x)
                val encryptionKey = "MY_ENCRYPTION_KEY"
                val encryptedBody = CryptoUtils.encryptData(
                    encryptionKey, originalBody
                )
                val encryptedContent = TextContent(encryptedBody, ContentType.Application.Json)
                proceedWith(encryptedContent)
            } else {
                proceedWith(request)
            }
        }
    }
}
    

2. encryption: Now, I'm trying to decrypt the response received from the server using the DecryptResponse plugin. Here's the code for the decryption install:

private fun HttpClientConfig<OkHttpConfig>.decryptInstall() {
    install("DecryptResponse") {
        receivePipeline.intercept(HttpReceivePipeline.After) { response ->
            val originalResponseReceived = response.body<String>()
            runCatching {
                val encryptionKey = "MY_DECRYPTION_KEY"
                val decryptData = CryptoUtils.decryptData(
                    encryptionKey, originalResponseReceived.toString().replace("\n", "")
                )
                val castedFromStringToObject =
                    Json.decodeFromString<LoginResponseDTO>(decryptData.orEmpty())
                // Now I have the decrypted data, how do I proceed with it?
                // I need to create an HttpResponse object with this decrypted data.
                // But I can't create an instance of an abstract class HttpResponse.
                // How can I create a new HttpResponse to proceed with the decrypted data?
            }
        }
    }
}

I tried decrypting the response using the DecryptResponse plugin and I successfully decrypted the content. However, I encountered an issue when trying to proceed with the decrypted data. Since HttpResponse is an abstract class, I couldn't directly create an instance of it to proceed with the decrypted data.

I was expecting to be able to create a new HttpResponse object with the decrypted data so that I can continue processing it. However, since HttpResponse is abstract, I'm not sure how to proceed. I need guidance or examples on how to create a new HttpResponse to proceed with the decrypted data after decryption.

Any insights or examples on how to achieve this would be greatly appreciated.

Thank you in advance for your help!


Solution

  • Instead of asking for examples, please just have a look at the actual source code.
    This is phase HttpResponsePipeline.Receive, but still the HttpResponsePipeline.

    scope.responsePipeline.intercept(HttpResponsePipeline.Receive) { (type, content) ->
        val method = context.request.method
        val contentLength = context.response.contentLength()
        if (contentLength == 0L) return@intercept
        if (contentLength == null && method == HttpMethod.Head) return@intercept
        if (content !is ByteReadChannel) return@intercept
        val response = with(plugin) {
            HttpResponseContainer(type, context.decode(context.response, content))
        }
        proceedWith(response)
    }
    

    File HttpResponsePipeline should have been read, because understanding the phases is crucial. The situation might be easier to understand and debug, when also adding a logging interceptor.

    // https://mvnrepository.com/artifact/io.ktor/ktor-client-logging-native
    implementation("io.ktor:ktor-client-logging-native:1.3.1")
    

    When intercepting at phase HttpReceivePipeline.After, this should be a plain-text response already (!!). With multiple interceptors it becomes obvious why selecting the proper phase is important. Phase After appears too late, see HttpResponsePipeline; Receive, Transform or Parse appear more meaningful than After. When using Transform on the request side already, I'd suggest to also use it on the response side.


    You may want to return an instance of DefaultHttpResponse or maybe HttpResponseContainer. See what proceedWith() at phase HttpResponsePipeline.Transform would accept to know which return-type is required. Even when the original response is immutable, you still can/should copy the headers from the original response (except content-lenght) + the decrypted response.body.

    As the statement response = with(plugin) suggests, one could plug-in the code, while overriding hooks transformRequestBody and transformResponseBody. This might be the proper approach.