Skip to content

Commit

Permalink
Verify HMAC when decrypting received payloads. (#11)
Browse files Browse the repository at this point in the history
* Verify HMAC when decrypting received payloads.
  • Loading branch information
prashanOS authored Sep 29, 2022
1 parent 3fe0ede commit f67314f
Show file tree
Hide file tree
Showing 6 changed files with 139 additions and 34 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -155,11 +155,4 @@ class MoshiPayloadAdapter(moshi: Moshi) : Session.PayloadAdapter {
"method" to method,
"params" to params
)

@JsonClass(generateAdapter = true)
data class EncryptedPayload(
@Json(name = "data") val data: String,
@Json(name = "iv") val iv: String,
@Json(name = "hmac") val hmac: String
)
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package org.walletconnect.impls

import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import com.squareup.moshi.Moshi
import org.bouncycastle.crypto.digests.SHA256Digest
import org.bouncycastle.crypto.engines.AESEngine
Expand All @@ -16,7 +18,7 @@ import java.security.SecureRandom

class MoshiPayloadEncryption(moshi: Moshi) : Session.PayloadEncryption {

private val encryptedPayloadAdapter = moshi.adapter(MoshiPayloadAdapter.EncryptedPayload::class.java)
private val encryptedPayloadAdapter = moshi.adapter(EncryptedPayload::class.java)

override fun encrypt(unencryptedPayloadJson: String, key: String): String {
val bytesData = unencryptedPayloadJson.toByteArray()
Expand Down Expand Up @@ -45,7 +47,7 @@ class MoshiPayloadEncryption(moshi: Moshi) : Session.PayloadEncryption {
hmac.doFinal(hmacResult, 0)

return encryptedPayloadAdapter.toJson(
MoshiPayloadAdapter.EncryptedPayload(
EncryptedPayload(
outBuf.toNoPrefixHexString(),
hmac = hmacResult.toNoPrefixHexString(),
iv = iv.toNoPrefixHexString()
Expand All @@ -56,20 +58,34 @@ class MoshiPayloadEncryption(moshi: Moshi) : Session.PayloadEncryption {
override fun decrypt(encryptedPayloadJson: String, key: String): String {
val encryptedPayload = encryptedPayloadAdapter.fromJson(encryptedPayloadJson) ?: throw IllegalArgumentException("Invalid json payload!")

// TODO verify hmac
val hexKey = decode(key)
val iv = decode(encryptedPayload.iv)
val encryptedData = decode(encryptedPayload.data)
val providedHmac = decode(encryptedPayload.hmac)

// verify hmac
with(HMac(SHA256Digest())) {
val hmacResult = ByteArray(macSize)
init(KeyParameter(hexKey))
update(encryptedData, 0, encryptedData.size)
update(iv, 0, iv.size)
doFinal(hmacResult, 0)

require(hmacResult.contentEquals(providedHmac)) { "HMAC does not match - expected: $hmacResult received: $providedHmac" }
}

// decrypt payload
val padding = PKCS7Padding()
val aes = PaddedBufferedBlockCipher(
CBCBlockCipher(AESEngine()),
padding
)
val ivAndKey = ParametersWithIV(
KeyParameter(decode(key)),
decode(encryptedPayload.iv)
KeyParameter(hexKey),
iv
)
aes.init(false, ivAndKey)

val encryptedData = decode(encryptedPayload.data)
val minSize = aes.getOutputSize(encryptedData.size)
val outBuf = ByteArray(minSize)
var len = aes.processBytes(encryptedData, 0, encryptedData.size, outBuf, 0)
Expand All @@ -79,4 +95,11 @@ class MoshiPayloadEncryption(moshi: Moshi) : Session.PayloadEncryption {
}

private fun createRandomBytes(i: Int) = ByteArray(i).also { SecureRandom().nextBytes(it) }
}
}

@JsonClass(generateAdapter = true)
data class EncryptedPayload(
@Json(name = "data") val data: String,
@Json(name = "iv") val iv: String,
@Json(name = "hmac") val hmac: String
)
10 changes: 5 additions & 5 deletions lib/src/main/kotlin/org/walletconnect/impls/WCSession.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class WCSession(
private val payloadAdapter: Session.PayloadAdapter,
private val payloadEncryption: Session.PayloadEncryption,
private val sessionStore: WCSessionStore,
private val messageLogger: Session.MessageLogger,
private val messageLogger: Session.MessageLogger? = null,
transportBuilder: Session.Transport.Builder,
clientMeta: Session.PeerMeta,
clientId: String? = null
Expand Down Expand Up @@ -97,7 +97,7 @@ class WCSession(
config.handshakeTopic, "sub", ""
)
transport.send(message)
messageLogger.log(message, isOwnMessage = true)
messageLogger?.log(message, isOwnMessage = true)
}
}

Expand Down Expand Up @@ -173,7 +173,7 @@ class WCSession(
clientData.id, "sub", ""
)
transport.send(message)
messageLogger.log(message, isOwnMessage = true)
messageLogger?.log(message, isOwnMessage = true)
}
Session.Transport.Status.Disconnected -> {
// no-op
Expand All @@ -198,7 +198,7 @@ class WCSession(
try {
val decryptedPayload = payloadEncryption.decrypt(message.payload, decryptionKey)
data = payloadAdapter.parse(decryptedPayload)
messageLogger.log(message.copy(payload = decryptedPayload), isOwnMessage = false)
messageLogger?.log(message.copy(payload = decryptedPayload), isOwnMessage = false)
} catch (e: Exception) {
handlePayloadError(e)
return
Expand Down Expand Up @@ -299,7 +299,7 @@ class WCSession(
}
val message = Session.Transport.Message(topic, "pub", payload)
transport.send(message)
messageLogger.log(message.copy(payload = unencryptedPayload), isOwnMessage = true)
messageLogger?.log(message.copy(payload = unencryptedPayload), isOwnMessage = true)
return true
}

Expand Down
10 changes: 1 addition & 9 deletions lib/src/test/java/org/walletconnect/TheUriParser.kt
Original file line number Diff line number Diff line change
@@ -1,15 +1,7 @@
package org.walletconnect

import com.squareup.moshi.Moshi
import okhttp3.OkHttpClient
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
import org.walletconnect.impls.FileWCSessionStore
import org.walletconnect.impls.MoshiPayloadAdapter
import org.walletconnect.impls.OkHttpTransport
import org.walletconnect.impls.WCSession
import java.io.File
import java.util.concurrent.TimeUnit
import org.junit.jupiter.api.Test

class TheUriParser {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ package org.walletconnect

import com.squareup.moshi.Moshi
import okhttp3.OkHttpClient
import org.junit.Test
import org.walletconnect.impls.FileWCSessionStore
import org.walletconnect.impls.MoshiPayloadAdapter
import org.walletconnect.impls.MoshiPayloadEncryption
import org.walletconnect.impls.OkHttpTransport
import org.walletconnect.impls.WCSession
import java.io.File
Expand All @@ -27,11 +27,12 @@ class WalletConnectBridgeRepositoryIntegrationTest {

val config = Session.Config.fromWCUri(uri).toFullyQualifiedConfig()
val session = WCSession(
config,
MoshiPayloadAdapter(moshi),
sessionStore,
OkHttpTransport.Builder(client, moshi),
Session.PeerMeta(name = "WC Unit Test")
config = config,
payloadAdapter = MoshiPayloadAdapter(moshi),
payloadEncryption = MoshiPayloadEncryption(moshi),
sessionStore = sessionStore,
transportBuilder = OkHttpTransport.Builder(client, moshi),
clientMeta = Session.PeerMeta(name = "WC Unit Test"),
)

session.addCallback(object : Session.Callback {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package org.walletconnect.impls

import com.squareup.moshi.Moshi
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import java.security.SecureRandom

class MoshiPayloadEncryptionTest {

private val moshi = Moshi.Builder().build()
private val payloadEncryption = MoshiPayloadEncryption(Moshi.Builder().build())
private val encryptedPayloadAdapter = moshi.adapter(EncryptedPayload::class.java)

@Test
fun `happy path encryption + decryption`() {
val key = generateKey()
val payloadJson =
"""
{
"id" : 123456,
"method" : "wc_sessionRequest",
}
""".trimIndent()
val encryptedPayload = payloadEncryption.encrypt(payloadJson, key)

val result = payloadEncryption.decrypt(encryptedPayload, key)

assertThat(result).isEqualTo(payloadJson)
}

@Test
fun `invalid hmac throws exception`() {
val key = generateKey()
// this is the original payload
val payloadJson =
"""
{
"id" : 123456,
"method" : "wc_sessionRequest",
}
""".trimIndent()
val mutatedPayload = with(encryptedPayloadAdapter.fromJson(payloadEncryption.encrypt(payloadJson, key))) {
requireNotNull(this)
encryptedPayloadAdapter.toJson(copy(hmac = hmac.dropLast(4)+"1234"))
}

assertThrows<IllegalArgumentException> {
payloadEncryption.decrypt(mutatedPayload, key)
}
}

@Test
fun `invalid iv throws exception`() {
val key = generateKey()
// this is the original payload
val payloadJson =
"""
{
"id" : 123456,
"method" : "wc_sessionRequest",
}
""".trimIndent()
val mutatedPayload = with(encryptedPayloadAdapter.fromJson(payloadEncryption.encrypt(payloadJson, key))) {
requireNotNull(this)
encryptedPayloadAdapter.toJson(copy(iv = iv.dropLast(4)+"1234"))
}

assertThrows<IllegalArgumentException> {
payloadEncryption.decrypt(mutatedPayload, key)
}
}

@Test
fun `invalid data throws exception`() {
val key = generateKey()
// this is the original payload
val payloadJson =
"""
{
"id" : 123456,
"method" : "wc_sessionRequest",
}
""".trimIndent()
val mutatedPayload = with(encryptedPayloadAdapter.fromJson(payloadEncryption.encrypt(payloadJson, key))) {
requireNotNull(this)
encryptedPayloadAdapter.toJson(copy(data = data.dropLast(4)+"1234"))
}

assertThrows<IllegalArgumentException> {
payloadEncryption.decrypt(mutatedPayload, key)
}
}

private fun generateKey() = ByteArray(32).also { SecureRandom().nextBytes(it) }.joinToString(separator = "") { "%02x".format(it) }
}

0 comments on commit f67314f

Please sign in to comment.