All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.jchanghong.http.okhttp.WiresharkExample.kt Maven / Gradle / Ivy

/*
 * Copyright (C) 2020 Square, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.github.jchanghong.http.okhttp

import com.github.jchanghong.http.okhttp.WireSharkListenerFactory.WireSharkKeyLoggerListener.Launch
import com.github.jchanghong.http.okhttp.WireSharkListenerFactory.WireSharkKeyLoggerListener.Launch.CommandLine
import com.github.jchanghong.http.okhttp.WireSharkListenerFactory.WireSharkKeyLoggerListener.Launch.Gui
import okhttp3.*
import okhttp3.TlsVersion.TLS_1_2
import okhttp3.TlsVersion.TLS_1_3
import okhttp3.internal.SuppressSignatureCheck
import okio.ByteString.Companion.toByteString
import java.io.File
import java.io.IOException
import java.lang.ProcessBuilder.Redirect
import java.util.logging.Handler
import java.util.logging.Level
import java.util.logging.LogRecord
import java.util.logging.Logger
import javax.crypto.SecretKey
import javax.net.ssl.SSLSession
import javax.net.ssl.SSLSocket

/**
 * Logs SSL keys to a log file, allowing Wireshark to decode traffic and be examined with http2
 * filter. The approach is to hook into JSSE log events for the messages between client and server
 * during handshake, and then take the agreed masterSecret from private fields of the session.
 *
 * Copy WireSharkKeyLoggerListener to your test code to use in development.
 *
 * This logs TLSv1.2 on a JVM (OpenJDK 11+) without any additional code.  For TLSv1.3
 * an existing external tool is required.
 *
 * See https://stackoverflow.com/questions/61929216/how-to-log-tlsv1-3-keys-in-jsse-for-wireshark-to-decode-traffic
 *
 * Steps to run in your own code
 *
 * 1. In your main method `WireSharkListenerFactory.register()`
 * 2. Create Listener factory `val eventListenerFactory = WireSharkListenerFactory(
logFile = File("/tmp/key.log"), tlsVersions = tlsVersions, launch = launch)`
 * 3. Register with `client.eventListenerFactory(eventListenerFactory)`
 * 4. Launch wireshark if not done externally `val process = eventListenerFactory.launchWireShark()`
 */
@SuppressSignatureCheck
class WireSharkListenerFactory(
    private val logFile: File,
    private val tlsVersions: List,
    private val launch: Launch? = null
) : EventListener.Factory {
    override fun create(call: Call): EventListener {
        return WireSharkKeyLoggerListener(logFile, launch == null)
    }

    fun launchWireShark(): Process? {
        when (launch) {
            null -> {
                if (tlsVersions.contains(TLS_1_2)) {
                    println("TLSv1.2 traffic will be logged automatically and available via wireshark")
                }

                if (tlsVersions.contains(TLS_1_3)) {
                    println("TLSv1.3 requires an external command run before first traffic is sent")
                    println("Follow instructions at https://github.com/neykov/extract-tls-secrets for TLSv1.3")
//          println("Pid: ${ProcessHandle.current().pid()}")

                    Thread.sleep(10000)
                }
            }
            CommandLine -> {
                return ProcessBuilder(
                    "tshark", "-l", "-V", "-o", "tls.keylog_file:$logFile", "-Y", "http2", "-O", "http2,tls"
                )
                    .redirectInput(File("/dev/null"))
                    .redirectOutput(Redirect.INHERIT)
                    .redirectError(Redirect.INHERIT)
                    .start()
            }
            Gui -> {
                return ProcessBuilder(
                    "nohup", "wireshark", "-o", "tls.keylog_file:$logFile", "-S", "-l", "-Y", "http2", "-k"
                )
                    .redirectInput(File("/dev/null"))
                    .redirectOutput(File("/dev/null"))
                    .redirectError(Redirect.INHERIT)
                    .start().also {
                        // Give it time to start collecting
                        Thread.sleep(2000)
                    }
            }
        }

        return null
    }

    class WireSharkKeyLoggerListener(
        private val logFile: File,
        private val verbose: Boolean = false
    ) : EventListener() {
        var random: String? = null
        lateinit var currentThread: Thread

        private val loggerHandler = object : Handler() {
            override fun publish(record: LogRecord) {
                // Try to avoid multi threading issues with concurrent requests
                if (Thread.currentThread() != currentThread) {
                    return
                }

                // https://timothybasanov.com/2016/05/26/java-pre-master-secret.html
                // https://security.stackexchange.com/questions/35639/decrypting-tls-in-wireshark-when-using-dhe-rsa-ciphersuites
                // https://stackoverflow.com/questions/36240279/how-do-i-extract-the-pre-master-secret-using-an-openssl-based-client

                // TLSv1.2 Events
                // Produced ClientHello handshake message
                // Consuming ServerHello handshake message
                // Consuming server Certificate handshake message
                // Consuming server CertificateStatus handshake message
                // Found trusted certificate
                // Consuming ECDH ServerKeyExchange handshake message
                // Consuming ServerHelloDone handshake message
                // Produced ECDHE ClientKeyExchange handshake message
                // Produced client Finished handshake message
                // Consuming server Finished handshake message
                // Produced ClientHello handshake message
                //
                // Raw write
                // Raw read
                // Plaintext before ENCRYPTION
                // Plaintext after DECRYPTION
                val message = record.message
                val parameters = record.parameters

                if (parameters != null && !message.startsWith("Raw") && !message.startsWith("Plaintext")) {
                    if (verbose) {
                        println(record.message)
                        println(record.parameters[0])
                    }

                    // JSSE logs additional messages as parameters that are not referenced in the log message.
                    val parameter = parameters[0] as String

                    if (message == "Produced ClientHello handshake message") {
                        random = readClientRandom(parameter)
                    }
                }
            }

            override fun flush() {}

            override fun close() {}
        }

        private fun readClientRandom(param: String): String? {
            val matchResult = randomRegex.find(param)

            return if (matchResult != null) {
                matchResult.groupValues[1].replace(" ", "")
            } else {
                null
            }
        }

        override fun secureConnectStart(call: Call) {
            // Register to capture "Produced ClientHello handshake message".
            currentThread = Thread.currentThread()
            logger.addHandler(loggerHandler)
        }

        override fun secureConnectEnd(
            call: Call,
            handshake: Handshake?
        ) {
            logger.removeHandler(loggerHandler)
        }

        override fun callEnd(call: Call) {
            // Cleanup log handler if failed.
            logger.removeHandler(loggerHandler)
        }

        override fun connectionAcquired(
            call: Call,
            connection: Connection
        ) {
            if (random != null) {
                val sslSocket = connection.socket() as SSLSocket
                val session = sslSocket.session

                val masterSecretHex = session.masterSecret?.encoded?.toByteString()
                    ?.hex()

                if (masterSecretHex != null) {
                    val keyLog = "CLIENT_RANDOM $random $masterSecretHex"

                    if (verbose) {
                        println(keyLog)
                    }
                    logFile.appendText("$keyLog\n")
                }
            }

            random = null
        }

        enum class Launch {
            Gui, CommandLine
        }
    }

    companion object {
        private lateinit var logger: Logger

        private val SSLSession.masterSecret: SecretKey?
            get() = javaClass.getDeclaredField("masterSecret")
                .apply {
                    isAccessible = true
                }
                .get(this) as? SecretKey

        val randomRegex = "\"random\"\\s+:\\s+\"([^\"]+)\"".toRegex()

        fun register() {
            // Enable JUL logging for SSL events, must be activated early or via -D option.
            System.setProperty("javax.net.debug", "")
            logger = Logger.getLogger("javax.net.ssl")
                .apply {
                    level = Level.FINEST
                    useParentHandlers = false
                }
        }
    }
}

@SuppressSignatureCheck
class WiresharkExample(tlsVersions: List, private val launch: Launch? = null) {
    private val connectionSpec =
        ConnectionSpec.Builder(ConnectionSpec.RESTRICTED_TLS)
            .tlsVersions(*tlsVersions.toTypedArray())
            .build()

    private val eventListenerFactory = WireSharkListenerFactory(
        logFile = File("/tmp/key.log"), tlsVersions = tlsVersions, launch = launch
    )

    val client = OkHttpClient.Builder()
        .connectionSpecs(listOf(connectionSpec))
        .eventListenerFactory(eventListenerFactory)
        .build()

    fun run() {
        // Launch wireshark in the background
        val process = eventListenerFactory.launchWireShark()

        val fbRequest = Request.Builder()
            .url("https://graph.facebook.com/robots.txt?s=fb")
            .build()
        val twitterRequest = Request.Builder()
            .url("https://api.twitter.com/robots.txt?s=tw")
            .build()
        val googleRequest = Request.Builder()
            .url("https://www.google.com/robots.txt?s=g")
            .build()

        try {
            for (i in 1..2) {
                // Space out traffic to make it easier to demarcate.
                sendTestRequest(fbRequest)
                Thread.sleep(1000)
                sendTestRequest(twitterRequest)
                Thread.sleep(1000)
                sendTestRequest(googleRequest)
                Thread.sleep(2000)
            }
        } finally {
            client.connectionPool.evictAll()
            client.dispatcher.executorService.shutdownNow()

            if (launch == CommandLine) {
                process?.destroyForcibly()
            }
        }
    }

    private fun sendTestRequest(request: Request) {
        try {
            if (this.launch != CommandLine) {
                println(request.url)
            }

            client.newCall(request)
                .execute()
                .use {
                    val firstLine = it.body!!.string()
                        .lines()
                        .first()
                    if (this.launch != CommandLine) {
                        println("${it.code} ${it.request.url.host} $firstLine")
                    }
                    Unit
                }
        } catch (e: IOException) {
            System.err.println(e)
        }
    }
}

fun main() {
    // Call this before anything else initialises the JSSE stack.
    WireSharkListenerFactory.register()

    val example = WiresharkExample(tlsVersions = listOf(TlsVersion.TLS_1_2), launch = CommandLine)
    example.run()
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy