Reputation: 91
I’m new to socket programming and am trying to connect to the WhatsApp WebSocket in an Android application. Below is the Kotlin code I wrote for performing the SSL handshake. The same code runs successfully in a JVM environment but fails when executed on Android. I am only trying it on Android 14 devices.
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.launch
import java.io.EOFException
import java.net.InetSocketAddress
import java.nio.ByteBuffer
import java.nio.channels.AsynchronousSocketChannel
import java.nio.channels.CompletionHandler
import java.util.Base64
import javax.net.ssl.SSLContext
import javax.net.ssl.SSLEngine
import javax.net.ssl.SSLEngineResult
import javax.net.ssl.SSLEngineResult.Status
object WhatsappTryHandshake {
const val WEB_SOCKET_HOST: String = "web.whatsapp.com"
const val WEB_SOCKET_PORT: Int = 443
const val KEY_LENGTH = 16
lateinit var socketChannel: AsynchronousSocketChannel
lateinit var sslEngine: SSLEngine
lateinit var clientKey: String
lateinit var sslReadBuffer: ByteBuffer
lateinit var netDataWrite: ByteBuffer
lateinit var sslOutputBuffer: ByteBuffer
private var isHandshakeCompleted = false
suspend fun connectToWhatsAppWebSocket() {
clientKey = Base64.getEncoder().encodeToString(ByteArray(KEY_LENGTH).apply { java.util.Random().nextBytes(this) })
// SSL context for secure connection
val sslContext = SSLContext.getInstance("TLSv1.3")
sslContext.init(null, null, null)
socketChannel = AsynchronousSocketChannel.open()
val address = InetSocketAddress(WEB_SOCKET_HOST, WEB_SOCKET_PORT)
socketChannel.connect(address).get()
// Set up SSL Engine
sslEngine = sslContext.createSSLEngine(WEB_SOCKET_HOST, WEB_SOCKET_PORT)
sslEngine.useClientMode = true
sslEngine.beginHandshake()
// SSL Handshake
netDataWrite = ByteBuffer.allocate(sslEngine.session.packetBufferSize)
sslReadBuffer = ByteBuffer.allocate(sslEngine.session.packetBufferSize)
sslReadBuffer.position(sslReadBuffer.limit())
sslOutputBuffer = ByteBuffer.allocate(sslEngine.session.packetBufferSize)
doHandshake()
}
private fun doHandshake(status: Status? = null) {
println("${sslEngine.handshakeStatus}")
when (sslEngine.handshakeStatus) {
SSLEngineResult.HandshakeStatus.NEED_WRAP -> {
netDataWrite.clear()
val result = sslEngine.wrap(sslOutputBuffer, netDataWrite)
val isHandshakeFinished = isHandshakeFinished(result, true)
netDataWrite.flip()
socketChannel.write(netDataWrite, null, object : CompletionHandler<Int?, Any?>{
override fun completed(result: Int?, attachment: Any?) {
println("completed: $result")
if (isHandshakeFinished){
finishSslHandshake()
}else{
doHandshake()
}
}
override fun failed(exc: Throwable?, attachment: Any?) {
println("failed: ${exc?.message}")
}
})
}
SSLEngineResult.HandshakeStatus.NEED_UNWRAP -> {
sslReadBuffer.compact()
if (status != Status.BUFFER_UNDERFLOW && sslReadBuffer.position() != 0){
sslReadBuffer.flip()
doSSlHandshakeUnwrapOperation()
}else{
readPlain(sslReadBuffer, true){
doSSlHandshakeUnwrapOperation()
}
}
}
SSLEngineResult.HandshakeStatus.FINISHED -> {
finishSslHandshake()
}
SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING -> {
finishSslHandshake()
println("Cannot complete Handshake")
}
SSLEngineResult.HandshakeStatus.NEED_TASK -> {
var runnable: Runnable? = sslEngine.delegatedTask
while (runnable != null) {
runnable.run()
runnable = sslEngine.delegatedTask
}
println("Handshake Status " + sslEngine.handshakeStatus )
doHandshake()
}
else -> throw IllegalStateException("Unknown handshake status")
}
}
private fun doSSlHandshakeUnwrapOperation() {
try {
val result: SSLEngineResult = sslEngine.unwrap(sslReadBuffer, sslOutputBuffer)
println(result.toString())
if (isHandshakeFinished(result, false)) {
finishSslHandshake()
} else {
doHandshake(result.status)
}
} catch (throwable: Throwable) {
throwable.printStackTrace()
finishSslHandshake()
}
}
private fun finishSslHandshake() {
isHandshakeCompleted = true
sslOutputBuffer.clear()
}
private fun isHandshakeFinished(result: SSLEngineResult, wrap: Boolean): Boolean {
val sslEngineStatus = result.status
check(!(sslEngineStatus != SSLEngineResult.Status.OK && (wrap || sslEngineStatus != SSLEngineResult.Status.BUFFER_UNDERFLOW))) { "SSL handshake operation failed with status: $sslEngineStatus" }
check(!(wrap && result.bytesConsumed() != 0)) {
throw IllegalStateException("SSL handshake operation failed with status: no bytes consumed")
}
check(!(!wrap && result.bytesProduced() != 0)) { throw IllegalStateException("SSL handshake operation failed with status: no bytes produced") }
val sslHandshakeStatus = result.handshakeStatus
return sslHandshakeStatus == SSLEngineResult.HandshakeStatus.FINISHED
}
private fun readPlain(buffer: ByteBuffer, lastRead: Boolean, proceed: () -> Unit) {
val outerCaller = RuntimeException()
socketChannel.read(buffer, null, object : CompletionHandler<Int, Any?> {
override fun completed(bytesRead: Int, attachment: Any?) {
if (bytesRead == -1) {
val eof = EOFException()
eof.addSuppressed(outerCaller)
println("Failed: ${eof.message}")
return
}
if (lastRead) {
buffer.flip()
}
proceed()
}
override fun failed(exc: Throwable, attachment: Any?) {
exc.addSuppressed(outerCaller)
println("Failed: ${exc.message}")
}
})
}
}
fun main(){
CoroutineScope(IO).launch {
WhatsappTryHandshake.connectToWhatsAppWebSocket()
}
while (true){
//..
}
}
JVM Environment Behavior
NEED_WRAP
, NEED_UNWRAP
, and NEED_TASK
as expected.Android Environment Behavior
The handshake fails with an exception at the last NEED_WRAP
stage.
The bytes produced in the final wrap
operation differ between JVM and Android.
This discrepancy leads to an IllegalStateException
with SSL handshake operation failed with status: no bytes consumed
.
NEED_WRAP
completed: 489
NEED_UNWRAP
Status = OK HandshakeStatus = NEED_TASK
bytesConsumed = 127 bytesProduced = 0
NEED_TASK
Handshake Status NEED_WRAP
NEED_WRAP
completed: 6
NEED_UNWRAP
Status = OK HandshakeStatus = NEED_UNWRAP
bytesConsumed = 6 bytesProduced = 0
NEED_UNWRAP
Status = OK HandshakeStatus = NEED_TASK
bytesConsumed = 1022 bytesProduced = 0
NEED_TASK
Handshake Status NEED_UNWRAP
NEED_UNWRAP
Status = OK HandshakeStatus = NEED_UNWRAP
bytesConsumed = 1522 bytesProduced = 0
NEED_UNWRAP
Status = BUFFER_UNDERFLOW HandshakeStatus = NEED_UNWRAP
bytesConsumed = 0 bytesProduced = 0
NEED_UNWRAP
Status = OK HandshakeStatus = NEED_TASK
bytesConsumed = 492 bytesProduced = 0
NEED_TASK
Handshake Status NEED_WRAP
NEED_WRAP
completed: 74
NEED_WRAP
completed: 517
NEED_UNWRAP
Status = OK HandshakeStatus = NEED_UNWRAP
bytesConsumed = 127 bytesProduced = 0
NEED_UNWRAP
Status = OK HandshakeStatus = NEED_UNWRAP
bytesConsumed = 6 bytesProduced = 0
NEED_UNWRAP
Status = OK HandshakeStatus = NEED_UNWRAP
bytesConsumed = 1022 bytesProduced = 0
NEED_UNWRAP
Status = OK HandshakeStatus = NEED_UNWRAP
bytesConsumed = 1522 bytesProduced = 0
NEED_UNWRAP
Status = OK HandshakeStatus = NEED_WRAP
bytesConsumed = 492 bytesProduced = 0
NEED_WRAP
java.lang.IllegalStateException: SSL handshake operation failed with status: no bytes consumed
at com.example.whatsappwrapperdemo.whatsapp.WhatsappTryHandshake.isHandshakeFinished(Whatsapp_transport_handshake.kt:145)
at com.example.whatsappwrapperdemo.whatsapp.WhatsappTryHandshake.doHandshake(Whatsapp_transport_handshake.kt:64)
at com.example.whatsappwrapperdemo.whatsapp.WhatsappTryHandshake.doSSlHandshakeUnwrapOperation(Whatsapp_transport_handshake.kt:127)
at com.example.whatsappwrapperdemo.whatsapp.WhatsappTryHandshake.doHandshake(Whatsapp_transport_handshake.kt:89)
at com.example.whatsappwrapperdemo.whatsapp.WhatsappTryHandshake.doSSlHandshakeUnwrapOperation(Whatsapp_transport_handshake.kt:127)
at com.example.whatsappwrapperdemo.whatsapp.WhatsappTryHandshake.doHandshake(Whatsapp_transport_handshake.kt:89)
at com.example.whatsappwrapperdemo.whatsapp.WhatsappTryHandshake.doSSlHandshakeUnwrapOperation(Whatsapp_transport_handshake.kt:127)
at com.example.whatsappwrapperdemo.whatsapp.WhatsappTryHandshake.doHandshake(Whatsapp_transport_handshake.kt:89)
at com.example.whatsappwrapperdemo.whatsapp.WhatsappTryHandshake.doSSlHandshakeUnwrapOperation(Whatsapp_transport_handshake.kt:127)
at com.example.whatsappwrapperdemo.whatsapp.WhatsappTryHandshake.doHandshake(Whatsapp_transport_handshake.kt:89)
at com.example.whatsappwrapperdemo.whatsapp.WhatsappTryHandshake.doSSlHandshakeUnwrapOperation(Whatsapp_transport_handshake.kt:127)
at com.example.whatsappwrapperdemo.whatsapp.WhatsappTryHandshake.access$doSSlHandshakeUnwrapOperation(Whatsapp_transport_handshake.kt:17)
at com.example.whatsappwrapperdemo.whatsapp.WhatsappTryHandshake$doHandshake$2.invoke(Whatsapp_transport_handshake.kt:93)
at com.example.whatsappwrapperdemo.whatsapp.WhatsappTryHandshake$doHandshake$2.invoke(Whatsapp_transport_handshake.kt:92)
at com.example.whatsappwrapperdemo.whatsapp.WhatsappTryHandshake$readPlain$1.completed(Whatsapp_transport_handshake.kt:168)
at com.example.whatsappwrapperdemo.whatsapp.WhatsappTryHandshake$readPlain$1.completed(Whatsapp_transport_handshake.kt:156)
at sun.nio.ch.Invoker.invokeUnchecked(Invoker.java:126)
at sun.nio.ch.UnixAsynchronousSocketChannelImpl.finishRead(UnixAsynchronousSocketChannelImpl.java:454)
at sun.nio.ch.UnixAsynchronousSocketChannelImpl.finish(UnixAsynchronousSocketChannelImpl.java:201)
at sun.nio.ch.UnixAsynchronousSocketChannelImpl.onEvent(UnixAsynchronousSocketChannelImpl.java:223)
at sun.nio.ch.EPollPort$EventHandlerTask.run(EPollPort.java:293)
at sun.nio.ch.AsynchronousChannelGroupImpl$1.run(AsynchronousChannelGroupImpl.java:112)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:644)
at java.lang.Thread.run(Thread.java:1012)
NEED_TASK
, which does not appear in Android.wrap
operation differ significantly between JVM and Android.certificates
so I added certificate files in the app too, adding network config file below.<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">web.whatsapp.com</domain>
<trust-anchors>
<certificates src="@raw/digicert_high_assurance_ev_root_ca" />
<certificates src="@raw/digicert_sha2_high_assurance_server_ca" />
<certificates src="@raw/whatsapp" />
<certificates src="system" />
</trust-anchors>
</domain-config>
</network-security-config>
Upvotes: 0
Views: 26