Android

DomainCertificatePinningFragment.kt

package dev.jons.example.pinning.android

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.graphics.Color
import android.os.Bundle
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.EditText
import android.widget.ListView
import android.widget.TextView
import androidx.core.widget.doOnTextChanged
import androidx.fragment.app.Fragment
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import dev.jons.example.pinning.android.GatherCertificateHashesIntentService.Companion.getCertistApiHashesForDomain
import dev.jons.example.pinning.android.GatherCertificateHashesIntentService.Companion.getLocalSocketHashesForDomain
import java.util.*

class DomainCertificatePinningFragment : Fragment() {
    private val localHashes: ArrayList<String?> = ArrayList()
    private val certistHashes: ArrayList<String?> = ArrayList()
    private var localSocketResultsAdapter: ArrayAdapter<String?>? = null
    private var certIstResultsAdapter: ArrayAdapter<String?>? = null
    private var rootView: View? = null
    private var editText: EditText? = null
    private var doHashesMatch: TextView? = null
    private val localResultsFilter = IntentFilter(
            GatherCertificateHashesIntentService.ACTION_RESULT_LOCAL_SOCKET_HASHES)
    private val certIstResultsFilter = IntentFilter(
            GatherCertificateHashesIntentService.ACTION_RESULT_CERTIST_HASHES)

    override fun onCreateView(inflater: LayoutInflater,
                              container: ViewGroup?,
                              savedInstanceState: Bundle?): View? {
        this.rootView = inflater.inflate(R.layout.fragment_certificate_pinning, container, false)
        localSocketResultsAdapter = ArrayAdapter(inflater.context,
                R.layout.expected_hash, R.id.list_view_item_hash, localHashes)
        certIstResultsAdapter = ArrayAdapter(inflater.context,
                R.layout.expected_hash, R.id.list_view_item_hash, certistHashes)
        this.editText = this.rootView?.findViewById(R.id.edit_text_first)
        this.editText?.setOnKeyListener { _: View?, keyCode: Int, event: KeyEvent ->
            if (event.action == KeyEvent.ACTION_DOWN &&
                    keyCode == KeyEvent.KEYCODE_ENTER) {
                gatherCertificates()
                return@setOnKeyListener true
            }
            return@setOnKeyListener false
        }
        this.editText?.doOnTextChanged { text, start, before, count ->

        }
        doHashesMatch = this.rootView?.findViewById(R.id.do_certificates_match_label)
        (this.rootView?.findViewById<View>(R.id.local_socket_results) as ListView).adapter = localSocketResultsAdapter
        (this.rootView?.findViewById<View>(R.id.certist_results) as ListView).adapter = certIstResultsAdapter
        this.rootView?.findViewById<View>(R.id.button_first)?.setOnClickListener { _: View? -> gatherCertificates() }
        return rootView
    }

    private fun gatherCertificates() {
        val manager = LocalBroadcastManager.getInstance(rootView!!.context)
        manager.registerReceiver(getResultsReceiver(manager,
                localSocketResultsAdapter, R.id.local_socket_results_label), localResultsFilter)
        manager.registerReceiver(getResultsReceiver(manager,
                certIstResultsAdapter, R.id.certist_results_label), certIstResultsFilter)
        val domain = editText!!.text.toString()
        getCertistApiHashesForDomain(rootView!!.context, domain)
        getLocalSocketHashesForDomain(rootView!!.context, domain)
    }

    private fun getResultsReceiver(
            manager: LocalBroadcastManager,
            adapter: ArrayAdapter<String?>?,
            label: Int): BroadcastReceiver {
        return object : BroadcastReceiver() {
            override fun onReceive(context: Context, intent: Intent) {
                rootView!!.findViewById<View>(label).visibility = View.VISIBLE
                manager.unregisterReceiver(this)
                adapter!!.clear()
                adapter.addAll(*intent.extras?.getStringArray(GatherCertificateHashesIntentService.RESULT_SHA256_HASHES))
                adapter.notifyDataSetChanged()
                allFound()
            }
        }
    }

    private fun grabHashesFromAdapter(adapter: ArrayAdapter<String?>?): List<String?> {
        val l: MutableList<String?> = ArrayList()
        (0 until adapter!!.count).forEach { i -> l += adapter.getItem(i) }
        return l
    }

    private fun allFound() {
        doHashesMatch!!.visibility = View.VISIBLE
        val certist = grabHashesFromAdapter(certIstResultsAdapter)
        val local = grabHashesFromAdapter(localSocketResultsAdapter)
        val matchPair = Pair(resources.getColor(android.R.color.holo_green_dark, null),
                R.string.certificate_hashes_match_label)
        val noMatchPair = Pair(Color.RED, R.string.certificate_hashes_donot_match_label)
        doHashesMatch!!.setTextColor(if (certist == local) matchPair.first else noMatchPair.first)
        doHashesMatch!!.setText(if (certist == local) matchPair.second else noMatchPair.second)
    }
}

GatherCertificateHashesIntentService.kt

package dev.jons.example.pinning.android

import android.app.IntentService
import android.content.Context
import android.content.Intent
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import org.json.JSONException
import org.json.JSONObject
import java.io.BufferedReader
import java.io.IOException
import java.io.InputStreamReader
import java.net.URL
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException
import java.security.cert.CertificateEncodingException
import javax.net.ssl.HttpsURLConnection
import javax.net.ssl.SSLSocketFactory

class GatherCertificateHashesIntentService : IntentService("RetrieveCertIstApiResults") {
    override fun onHandleIntent(intent: Intent?) {
        if (intent != null) {
            val action = intent.action
            val hostName = intent.getStringExtra(EXTRAS_HOST_NAME)
            if (hostName != null) {
                when {
                    ACTION_GET_CERTIST_HASHES == action -> {
                        getCertificateHashesFromCertIstApi(hostName)
                    }
                    ACTION_GET_LOCAL_SOCKET_HASHES == action -> {
                        getCertificateHashesFromLocalSocket(hostName)
                    }
                    else -> {
                        throw UnsupportedOperationException(String.format("ERROR UNKNOWN ACTION: %s", action))
                    }
                }
            }
        }
    }

    private fun getCertificateHashesFromCertIstApi(hostName: String) {
        try {
            val format = String.format("https://api.cert.ist/%s", hostName)
            val urlConnection = URL(format).openConnection() as HttpsURLConnection
            urlConnection.requestMethod = "GET"
            urlConnection.readTimeout = 10 * 1000
            urlConnection.connectTimeout = 10 * 1000
            urlConnection.sslSocketFactory = SSLSocketFactory.getDefault() as SSLSocketFactory
            urlConnection.connect()
            val total = StringBuilder()
            urlConnection.inputStream.use { stream ->
                BufferedReader(InputStreamReader(stream)).use { reader ->
                    var line: String?
                    while (reader.readLine().also { line = it } != null) {
                        total.append(line).append('\n')
                    }
                }
            }
            val root = JSONObject(total.toString())
            val chain = root.getJSONArray("chain")
            val sha256Hashes = arrayOfNulls<String>(chain.length())
            (0 until chain.length()).forEach { i ->
                val certificateInChain = chain.getJSONObject(i)
                val der = certificateInChain.getJSONObject("der")
                val hashes = der.getJSONObject("hashes")
                sha256Hashes[i] = hashes.getString("sha256")
            }
            val data = Intent(ACTION_RESULT_CERTIST_HASHES)
            data.putExtra(RESULT_SHA256_HASHES, sha256Hashes)
            LocalBroadcastManager.getInstance(applicationContext).sendBroadcast(data)
        } catch (e: IOException) {
            e.printStackTrace()
        } catch (e: JSONException) {
            e.printStackTrace()
        }
    }

    private fun getCertificateHashesFromLocalSocket(hostName: String) {
        try {
            val url = URL(String.format("https://%s", hostName))
            val urlConnection = url.openConnection() as HttpsURLConnection
            urlConnection.requestMethod = "GET"
            urlConnection.readTimeout = 10 * 1000
            urlConnection.connectTimeout = 10 * 1000
            urlConnection.sslSocketFactory = SSLSocketFactory.getDefault() as SSLSocketFactory
            urlConnection.connect()
            val hashes = arrayOfNulls<String>(urlConnection.serverCertificates.size)
            urlConnection.serverCertificates.indices.forEach { i ->
                val certificate = urlConnection.serverCertificates[i]
                val encoded = certificate.encoded
                val digest = MessageDigest.getInstance("SHA-256")
                val hash = digest.digest(encoded)
                val hex = StringBuilder(hash.size * 2)
                for (b in hash) hex.append(String.format("%02x", b))
                hashes[i] = hex.toString()
            }
            val data = Intent(ACTION_RESULT_LOCAL_SOCKET_HASHES)
            data.putExtra(RESULT_SHA256_HASHES, hashes)
            LocalBroadcastManager.getInstance(applicationContext).sendBroadcast(data)
        } catch (e: IOException) {
            e.printStackTrace()
        } catch (e: CertificateEncodingException) {
            e.printStackTrace()
        } catch (e: NoSuchAlgorithmException) {
            e.printStackTrace()
        }
    }

    companion object {
        const val ACTION_GET_CERTIST_HASHES = "dev.jons.example.pinning.android.action.GET_CERTIST_HASHES"
        const val ACTION_GET_LOCAL_SOCKET_HASHES = "dev.jons.example.pinning.android.action.GET_LOCAL_SOCKET_HASHES"
        const val ACTION_RESULT_LOCAL_SOCKET_HASHES = "dev.jons.example.pinning.android.action.RESULT_LOCAL_SOCKET_HASHES"
        const val ACTION_RESULT_CERTIST_HASHES = "dev.jons.example.pinning.android.action.RESULT_CERTIST_HASHES"
        const val EXTRAS_HOST_NAME = "dev.jons.example.pinning.android.extra.HOST_NAME"
        const val RESULT_SHA256_HASHES = "dev.jons.example.pinning.android.result.RESULT_SHA256_HASHES"
        @JvmStatic
        fun getCertistApiHashesForDomain(context: Context, domain: String?) {
            val intent = Intent(context, GatherCertificateHashesIntentService::class.java)
            intent.action = ACTION_GET_CERTIST_HASHES
            intent.putExtra(EXTRAS_HOST_NAME, domain)
            context.startService(intent)
        }

        @JvmStatic
        fun getLocalSocketHashesForDomain(context: Context, domain: String?) {
            val intent = Intent(context, GatherCertificateHashesIntentService::class.java)
            intent.action = ACTION_GET_LOCAL_SOCKET_HASHES
            intent.putExtra(EXTRAS_HOST_NAME, domain)
            context.startService(intent)
        }
    }
}

strings.xml

<resources>
    <string name="app_name">Certificate pinning</string>
    <string name="next">Validate Certificates</string>

    <string name="initial_domain">urip.io</string>
    <string name="local_socket_results_label">Hashes as seen from local socket</string>
    <string name="certist_results_label">Hashes as cert.ist sees them on the public internet</string>
    <string name="certificate_hashes_match_label">Certificates from CertIst api and local sockets matched</string>
    <string name="certificate_hashes_donot_match_label">Certificates from CertIst api and local sockets DO NOT matched</string>
    <string name="domain_label">Domain:</string>
</resources>


Github