Skip to content

Commit f67314f

Browse files
authored
Verify HMAC when decrypting received payloads. (#11)
* Verify HMAC when decrypting received payloads.
1 parent 3fe0ede commit f67314f

File tree

6 files changed

+139
-34
lines changed

6 files changed

+139
-34
lines changed

lib/src/main/kotlin/org/walletconnect/impls/MoshiPayloadAdapter.kt

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -155,11 +155,4 @@ class MoshiPayloadAdapter(moshi: Moshi) : Session.PayloadAdapter {
155155
"method" to method,
156156
"params" to params
157157
)
158-
159-
@JsonClass(generateAdapter = true)
160-
data class EncryptedPayload(
161-
@Json(name = "data") val data: String,
162-
@Json(name = "iv") val iv: String,
163-
@Json(name = "hmac") val hmac: String
164-
)
165158
}

lib/src/main/kotlin/org/walletconnect/impls/MoshiPayloadEncryption.kt

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package org.walletconnect.impls
22

3+
import com.squareup.moshi.Json
4+
import com.squareup.moshi.JsonClass
35
import com.squareup.moshi.Moshi
46
import org.bouncycastle.crypto.digests.SHA256Digest
57
import org.bouncycastle.crypto.engines.AESEngine
@@ -16,7 +18,7 @@ import java.security.SecureRandom
1618

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

19-
private val encryptedPayloadAdapter = moshi.adapter(MoshiPayloadAdapter.EncryptedPayload::class.java)
21+
private val encryptedPayloadAdapter = moshi.adapter(EncryptedPayload::class.java)
2022

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

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

59-
// TODO verify hmac
61+
val hexKey = decode(key)
62+
val iv = decode(encryptedPayload.iv)
63+
val encryptedData = decode(encryptedPayload.data)
64+
val providedHmac = decode(encryptedPayload.hmac)
65+
66+
// verify hmac
67+
with(HMac(SHA256Digest())) {
68+
val hmacResult = ByteArray(macSize)
69+
init(KeyParameter(hexKey))
70+
update(encryptedData, 0, encryptedData.size)
71+
update(iv, 0, iv.size)
72+
doFinal(hmacResult, 0)
6073

74+
require(hmacResult.contentEquals(providedHmac)) { "HMAC does not match - expected: $hmacResult received: $providedHmac" }
75+
}
76+
77+
// decrypt payload
6178
val padding = PKCS7Padding()
6279
val aes = PaddedBufferedBlockCipher(
6380
CBCBlockCipher(AESEngine()),
6481
padding
6582
)
6683
val ivAndKey = ParametersWithIV(
67-
KeyParameter(decode(key)),
68-
decode(encryptedPayload.iv)
84+
KeyParameter(hexKey),
85+
iv
6986
)
7087
aes.init(false, ivAndKey)
7188

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

8197
private fun createRandomBytes(i: Int) = ByteArray(i).also { SecureRandom().nextBytes(it) }
82-
}
98+
}
99+
100+
@JsonClass(generateAdapter = true)
101+
data class EncryptedPayload(
102+
@Json(name = "data") val data: String,
103+
@Json(name = "iv") val iv: String,
104+
@Json(name = "hmac") val hmac: String
105+
)

lib/src/main/kotlin/org/walletconnect/impls/WCSession.kt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ class WCSession(
1313
private val payloadAdapter: Session.PayloadAdapter,
1414
private val payloadEncryption: Session.PayloadEncryption,
1515
private val sessionStore: WCSessionStore,
16-
private val messageLogger: Session.MessageLogger,
16+
private val messageLogger: Session.MessageLogger? = null,
1717
transportBuilder: Session.Transport.Builder,
1818
clientMeta: Session.PeerMeta,
1919
clientId: String? = null
@@ -97,7 +97,7 @@ class WCSession(
9797
config.handshakeTopic, "sub", ""
9898
)
9999
transport.send(message)
100-
messageLogger.log(message, isOwnMessage = true)
100+
messageLogger?.log(message, isOwnMessage = true)
101101
}
102102
}
103103

@@ -173,7 +173,7 @@ class WCSession(
173173
clientData.id, "sub", ""
174174
)
175175
transport.send(message)
176-
messageLogger.log(message, isOwnMessage = true)
176+
messageLogger?.log(message, isOwnMessage = true)
177177
}
178178
Session.Transport.Status.Disconnected -> {
179179
// no-op
@@ -198,7 +198,7 @@ class WCSession(
198198
try {
199199
val decryptedPayload = payloadEncryption.decrypt(message.payload, decryptionKey)
200200
data = payloadAdapter.parse(decryptedPayload)
201-
messageLogger.log(message.copy(payload = decryptedPayload), isOwnMessage = false)
201+
messageLogger?.log(message.copy(payload = decryptedPayload), isOwnMessage = false)
202202
} catch (e: Exception) {
203203
handlePayloadError(e)
204204
return
@@ -299,7 +299,7 @@ class WCSession(
299299
}
300300
val message = Session.Transport.Message(topic, "pub", payload)
301301
transport.send(message)
302-
messageLogger.log(message.copy(payload = unencryptedPayload), isOwnMessage = true)
302+
messageLogger?.log(message.copy(payload = unencryptedPayload), isOwnMessage = true)
303303
return true
304304
}
305305

lib/src/test/java/org/walletconnect/TheUriParser.kt

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,7 @@
11
package org.walletconnect
22

3-
import com.squareup.moshi.Moshi
4-
import okhttp3.OkHttpClient
53
import org.assertj.core.api.Assertions.assertThat
6-
import org.junit.Test
7-
import org.walletconnect.impls.FileWCSessionStore
8-
import org.walletconnect.impls.MoshiPayloadAdapter
9-
import org.walletconnect.impls.OkHttpTransport
10-
import org.walletconnect.impls.WCSession
11-
import java.io.File
12-
import java.util.concurrent.TimeUnit
4+
import org.junit.jupiter.api.Test
135

146
class TheUriParser {
157

lib/src/test/java/org/walletconnect/WalletConnectBridgeRepositoryIntegrationTest.kt

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ package org.walletconnect
22

33
import com.squareup.moshi.Moshi
44
import okhttp3.OkHttpClient
5-
import org.junit.Test
65
import org.walletconnect.impls.FileWCSessionStore
76
import org.walletconnect.impls.MoshiPayloadAdapter
7+
import org.walletconnect.impls.MoshiPayloadEncryption
88
import org.walletconnect.impls.OkHttpTransport
99
import org.walletconnect.impls.WCSession
1010
import java.io.File
@@ -27,11 +27,12 @@ class WalletConnectBridgeRepositoryIntegrationTest {
2727

2828
val config = Session.Config.fromWCUri(uri).toFullyQualifiedConfig()
2929
val session = WCSession(
30-
config,
31-
MoshiPayloadAdapter(moshi),
32-
sessionStore,
33-
OkHttpTransport.Builder(client, moshi),
34-
Session.PeerMeta(name = "WC Unit Test")
30+
config = config,
31+
payloadAdapter = MoshiPayloadAdapter(moshi),
32+
payloadEncryption = MoshiPayloadEncryption(moshi),
33+
sessionStore = sessionStore,
34+
transportBuilder = OkHttpTransport.Builder(client, moshi),
35+
clientMeta = Session.PeerMeta(name = "WC Unit Test"),
3536
)
3637

3738
session.addCallback(object : Session.Callback {
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package org.walletconnect.impls
2+
3+
import com.squareup.moshi.Moshi
4+
import org.assertj.core.api.Assertions.assertThat
5+
import org.junit.jupiter.api.Test
6+
import org.junit.jupiter.api.assertThrows
7+
import java.security.SecureRandom
8+
9+
class MoshiPayloadEncryptionTest {
10+
11+
private val moshi = Moshi.Builder().build()
12+
private val payloadEncryption = MoshiPayloadEncryption(Moshi.Builder().build())
13+
private val encryptedPayloadAdapter = moshi.adapter(EncryptedPayload::class.java)
14+
15+
@Test
16+
fun `happy path encryption + decryption`() {
17+
val key = generateKey()
18+
val payloadJson =
19+
"""
20+
{
21+
"id" : 123456,
22+
"method" : "wc_sessionRequest",
23+
}
24+
""".trimIndent()
25+
val encryptedPayload = payloadEncryption.encrypt(payloadJson, key)
26+
27+
val result = payloadEncryption.decrypt(encryptedPayload, key)
28+
29+
assertThat(result).isEqualTo(payloadJson)
30+
}
31+
32+
@Test
33+
fun `invalid hmac throws exception`() {
34+
val key = generateKey()
35+
// this is the original payload
36+
val payloadJson =
37+
"""
38+
{
39+
"id" : 123456,
40+
"method" : "wc_sessionRequest",
41+
}
42+
""".trimIndent()
43+
val mutatedPayload = with(encryptedPayloadAdapter.fromJson(payloadEncryption.encrypt(payloadJson, key))) {
44+
requireNotNull(this)
45+
encryptedPayloadAdapter.toJson(copy(hmac = hmac.dropLast(4)+"1234"))
46+
}
47+
48+
assertThrows<IllegalArgumentException> {
49+
payloadEncryption.decrypt(mutatedPayload, key)
50+
}
51+
}
52+
53+
@Test
54+
fun `invalid iv throws exception`() {
55+
val key = generateKey()
56+
// this is the original payload
57+
val payloadJson =
58+
"""
59+
{
60+
"id" : 123456,
61+
"method" : "wc_sessionRequest",
62+
}
63+
""".trimIndent()
64+
val mutatedPayload = with(encryptedPayloadAdapter.fromJson(payloadEncryption.encrypt(payloadJson, key))) {
65+
requireNotNull(this)
66+
encryptedPayloadAdapter.toJson(copy(iv = iv.dropLast(4)+"1234"))
67+
}
68+
69+
assertThrows<IllegalArgumentException> {
70+
payloadEncryption.decrypt(mutatedPayload, key)
71+
}
72+
}
73+
74+
@Test
75+
fun `invalid data throws exception`() {
76+
val key = generateKey()
77+
// this is the original payload
78+
val payloadJson =
79+
"""
80+
{
81+
"id" : 123456,
82+
"method" : "wc_sessionRequest",
83+
}
84+
""".trimIndent()
85+
val mutatedPayload = with(encryptedPayloadAdapter.fromJson(payloadEncryption.encrypt(payloadJson, key))) {
86+
requireNotNull(this)
87+
encryptedPayloadAdapter.toJson(copy(data = data.dropLast(4)+"1234"))
88+
}
89+
90+
assertThrows<IllegalArgumentException> {
91+
payloadEncryption.decrypt(mutatedPayload, key)
92+
}
93+
}
94+
95+
private fun generateKey() = ByteArray(32).also { SecureRandom().nextBytes(it) }.joinToString(separator = "") { "%02x".format(it) }
96+
}

0 commit comments

Comments
 (0)