Skip to content

Commit 3fe0ede

Browse files
authored
Split out payload field JSON construction from encryption/decryption.… (WalletConnect#10)
* Split out payload field JSON construction from encryption/decryption. Also introduce MessageLogger interface that logs messages with unencrypted payloads.
1 parent d651083 commit 3fe0ede

File tree

5 files changed

+137
-99
lines changed

5 files changed

+137
-99
lines changed

lib/src/main/kotlin/org/walletconnect/Session.kt

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,10 @@ interface Session {
7373
}
7474
}
7575

76+
interface MessageLogger {
77+
fun log(message: Session.Transport.Message, isOwnMessage: Boolean)
78+
}
79+
7680
interface Callback {
7781
fun onStatus(status: Status)
7882
fun onMethodCall(call: MethodCall)
@@ -90,9 +94,21 @@ interface Session {
9094
data class TransportError(override val cause: Throwable) : RuntimeException("Transport exception caused by $cause", cause)
9195

9296
interface PayloadAdapter {
93-
fun parse(payload: String, key: String): MethodCall
94-
fun prepare(data: MethodCall, key: String): String
97+
/**
98+
* Takes in the decrypted payload JSON and returns the parsed [MethodCall].
99+
*/
100+
fun parse(decryptedPayload: String): MethodCall
101+
102+
/**
103+
* Takes in a [MethodCall] and returns the unencrypted payload JSON.
104+
*/
105+
fun prepare(data: MethodCall): String
106+
107+
}
95108

109+
interface PayloadEncryption {
110+
fun encrypt(unencryptedPayloadJson: String, key: String): String
111+
fun decrypt(encryptedPayloadJson: String, key: String): String
96112
}
97113

98114
interface Transport {
@@ -121,7 +137,7 @@ interface Session {
121137
fun build(
122138
url: String,
123139
statusHandler: (Status) -> Unit,
124-
messageHandler: (Message) -> Unit
140+
messageHandler: (Message) -> Unit,
125141
): Transport
126142
}
127143

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

Lines changed: 9 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,11 @@ import com.squareup.moshi.Json
44
import com.squareup.moshi.JsonClass
55
import com.squareup.moshi.Moshi
66
import com.squareup.moshi.Types
7-
import org.bouncycastle.crypto.digests.SHA256Digest
8-
import org.bouncycastle.crypto.engines.AESEngine
9-
import org.bouncycastle.crypto.macs.HMac
10-
import org.bouncycastle.crypto.modes.CBCBlockCipher
11-
import org.bouncycastle.crypto.paddings.PKCS7Padding
12-
import org.bouncycastle.crypto.paddings.PaddedBufferedBlockCipher
13-
import org.bouncycastle.crypto.params.KeyParameter
14-
import org.bouncycastle.crypto.params.ParametersWithIV
15-
import org.komputing.khex.decode
16-
import org.komputing.khex.extensions.toNoPrefixHexString
177
import org.walletconnect.Session
188
import org.walletconnect.types.*
19-
import java.security.SecureRandom
209

2110
class MoshiPayloadAdapter(moshi: Moshi) : Session.PayloadAdapter {
2211

23-
private val payloadAdapter = moshi.adapter(EncryptedPayload::class.java)
2412
private val mapAdapter = moshi.adapter<Map<String, Any?>>(
2513
Types.newParameterizedType(
2614
Map::class.java,
@@ -29,74 +17,19 @@ class MoshiPayloadAdapter(moshi: Moshi) : Session.PayloadAdapter {
2917
)
3018
)
3119

32-
private fun createRandomBytes(i: Int) = ByteArray(i).also { SecureRandom().nextBytes(it) }
33-
34-
override fun parse(payload: String, key: String): Session.MethodCall {
35-
val encryptedPayload = payloadAdapter.fromJson(payload) ?: throw IllegalArgumentException("Invalid json payload!")
36-
37-
// TODO verify hmac
38-
39-
val padding = PKCS7Padding()
40-
val aes = PaddedBufferedBlockCipher(
41-
CBCBlockCipher(AESEngine()),
42-
padding
43-
)
44-
val ivAndKey = ParametersWithIV(
45-
KeyParameter(decode(key)),
46-
decode(encryptedPayload.iv)
47-
)
48-
aes.init(false, ivAndKey)
49-
50-
val encryptedData = decode(encryptedPayload.data)
51-
val minSize = aes.getOutputSize(encryptedData.size)
52-
val outBuf = ByteArray(minSize)
53-
var len = aes.processBytes(encryptedData, 0, encryptedData.size, outBuf, 0)
54-
len += aes.doFinal(outBuf, len)
55-
56-
return outBuf.copyOf(len).toMethodCall()
20+
override fun parse(decryptedPayloadJson: String): Session.MethodCall {
21+
return decryptedPayloadJson.toMethodCall()
5722
}
5823

59-
override fun prepare(data: Session.MethodCall, key: String): String {
60-
val bytesData = data.toBytes()
61-
val hexKey = decode(key)
62-
val iv = createRandomBytes(16)
63-
64-
val padding = PKCS7Padding()
65-
val aes = PaddedBufferedBlockCipher(
66-
CBCBlockCipher(AESEngine()),
67-
padding
68-
)
69-
aes.init(true, ParametersWithIV(KeyParameter(hexKey), iv))
70-
71-
val minSize = aes.getOutputSize(bytesData.size)
72-
val outBuf = ByteArray(minSize)
73-
val length1 = aes.processBytes(bytesData, 0, bytesData.size, outBuf, 0)
74-
aes.doFinal(outBuf, length1)
75-
76-
77-
val hmac = HMac(SHA256Digest())
78-
hmac.init(KeyParameter(hexKey))
79-
80-
val hmacResult = ByteArray(hmac.macSize)
81-
hmac.update(outBuf, 0, outBuf.size)
82-
hmac.update(iv, 0, iv.size)
83-
hmac.doFinal(hmacResult, 0)
84-
85-
return payloadAdapter.toJson(
86-
EncryptedPayload(
87-
outBuf.toNoPrefixHexString(),
88-
hmac = hmacResult.toNoPrefixHexString(),
89-
iv = iv.toNoPrefixHexString()
90-
)
91-
)
24+
override fun prepare(data: Session.MethodCall): String {
25+
return data.toJson()
9226
}
9327

9428
/**
9529
* Convert FROM request bytes
9630
*/
97-
private fun ByteArray.toMethodCall(): Session.MethodCall =
98-
String(this).let { json ->
99-
mapAdapter.fromJson(json)?.let {
31+
private fun String.toMethodCall(): Session.MethodCall =
32+
mapAdapter.fromJson(this)?.let {
10033
try {
10134
val method = it["method"]
10235
when (method) {
@@ -108,10 +41,9 @@ class MoshiPayloadAdapter(moshi: Moshi) : Session.PayloadAdapter {
10841
else -> it.toCustom()
10942
}
11043
} catch (e: Exception) {
111-
throw Session.MethodCallException.InvalidRequest(it.getId(), "$json (${e.message ?: "Unknown error"})")
44+
throw Session.MethodCallException.InvalidRequest(it.getId(), "$this (${e.message ?: "Unknown error"})")
11245
}
11346
} ?: throw IllegalArgumentException("Invalid json")
114-
}
11547

11648
private fun Map<String, *>.toSessionUpdate(): Session.MethodCall.SessionUpdate {
11749
val params = this["params"] as? List<*> ?: throw IllegalArgumentException("params missing")
@@ -163,7 +95,7 @@ class MoshiPayloadAdapter(moshi: Moshi) : Session.PayloadAdapter {
16395
/**
16496
* Convert INTO request bytes
16597
*/
166-
private fun Session.MethodCall.toBytes() =
98+
private fun Session.MethodCall.toJson() =
16799
mapAdapter.toJson(
168100
when (this) {
169101
is Session.MethodCall.SessionRequest -> this.toMap()
@@ -173,7 +105,7 @@ class MoshiPayloadAdapter(moshi: Moshi) : Session.PayloadAdapter {
173105
is Session.MethodCall.SignMessage -> this.toMap()
174106
is Session.MethodCall.Custom -> this.toMap()
175107
}
176-
).toByteArray()
108+
)
177109

178110
private fun Session.MethodCall.SessionRequest.toMap() =
179111
jsonRpc(id, "wc_sessionRequest", peer.intoMap())
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package org.walletconnect.impls
2+
3+
import com.squareup.moshi.Moshi
4+
import org.bouncycastle.crypto.digests.SHA256Digest
5+
import org.bouncycastle.crypto.engines.AESEngine
6+
import org.bouncycastle.crypto.macs.HMac
7+
import org.bouncycastle.crypto.modes.CBCBlockCipher
8+
import org.bouncycastle.crypto.paddings.PKCS7Padding
9+
import org.bouncycastle.crypto.paddings.PaddedBufferedBlockCipher
10+
import org.bouncycastle.crypto.params.KeyParameter
11+
import org.bouncycastle.crypto.params.ParametersWithIV
12+
import org.komputing.khex.decode
13+
import org.komputing.khex.extensions.toNoPrefixHexString
14+
import org.walletconnect.Session
15+
import java.security.SecureRandom
16+
17+
class MoshiPayloadEncryption(moshi: Moshi) : Session.PayloadEncryption {
18+
19+
private val encryptedPayloadAdapter = moshi.adapter(MoshiPayloadAdapter.EncryptedPayload::class.java)
20+
21+
override fun encrypt(unencryptedPayloadJson: String, key: String): String {
22+
val bytesData = unencryptedPayloadJson.toByteArray()
23+
val hexKey = decode(key)
24+
val iv = createRandomBytes(16)
25+
26+
val padding = PKCS7Padding()
27+
val aes = PaddedBufferedBlockCipher(
28+
CBCBlockCipher(AESEngine()),
29+
padding
30+
)
31+
aes.init(true, ParametersWithIV(KeyParameter(hexKey), iv))
32+
33+
val minSize = aes.getOutputSize(bytesData.size)
34+
val outBuf = ByteArray(minSize)
35+
val length1 = aes.processBytes(bytesData, 0, bytesData.size, outBuf, 0)
36+
aes.doFinal(outBuf, length1)
37+
38+
39+
val hmac = HMac(SHA256Digest())
40+
hmac.init(KeyParameter(hexKey))
41+
42+
val hmacResult = ByteArray(hmac.macSize)
43+
hmac.update(outBuf, 0, outBuf.size)
44+
hmac.update(iv, 0, iv.size)
45+
hmac.doFinal(hmacResult, 0)
46+
47+
return encryptedPayloadAdapter.toJson(
48+
MoshiPayloadAdapter.EncryptedPayload(
49+
outBuf.toNoPrefixHexString(),
50+
hmac = hmacResult.toNoPrefixHexString(),
51+
iv = iv.toNoPrefixHexString()
52+
)
53+
)
54+
}
55+
56+
override fun decrypt(encryptedPayloadJson: String, key: String): String {
57+
val encryptedPayload = encryptedPayloadAdapter.fromJson(encryptedPayloadJson) ?: throw IllegalArgumentException("Invalid json payload!")
58+
59+
// TODO verify hmac
60+
61+
val padding = PKCS7Padding()
62+
val aes = PaddedBufferedBlockCipher(
63+
CBCBlockCipher(AESEngine()),
64+
padding
65+
)
66+
val ivAndKey = ParametersWithIV(
67+
KeyParameter(decode(key)),
68+
decode(encryptedPayload.iv)
69+
)
70+
aes.init(false, ivAndKey)
71+
72+
val encryptedData = decode(encryptedPayload.data)
73+
val minSize = aes.getOutputSize(encryptedData.size)
74+
val outBuf = ByteArray(minSize)
75+
var len = aes.processBytes(encryptedData, 0, encryptedData.size, outBuf, 0)
76+
len += aes.doFinal(outBuf, len)
77+
78+
return String(outBuf.copyOf(len))
79+
}
80+
81+
private fun createRandomBytes(i: Int) = ByteArray(i).also { SecureRandom().nextBytes(it) }
82+
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,12 +121,12 @@ class OkHttpTransport(
121121
}
122122
}
123123

124-
class Builder(val client: OkHttpClient, val moshi: Moshi) :
124+
class Builder(private val client: OkHttpClient, private val moshi: Moshi) :
125125
Session.Transport.Builder {
126126
override fun build(
127127
url: String,
128128
statusHandler: (Session.Transport.Status) -> Unit,
129-
messageHandler: (Session.Transport.Message) -> Unit
129+
messageHandler: (Session.Transport.Message) -> Unit,
130130
): Session.Transport =
131131
OkHttpTransport(client, url, statusHandler, messageHandler, moshi)
132132

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

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,14 @@ import java.util.*
99
import java.util.concurrent.ConcurrentHashMap
1010

1111
class WCSession(
12-
private val config: Session.FullyQualifiedConfig,
13-
private val payloadAdapter: Session.PayloadAdapter,
14-
private val sessionStore: WCSessionStore,
15-
transportBuilder: Session.Transport.Builder,
16-
clientMeta: Session.PeerMeta,
17-
clientId: String? = null
12+
private val config: Session.FullyQualifiedConfig,
13+
private val payloadAdapter: Session.PayloadAdapter,
14+
private val payloadEncryption: Session.PayloadEncryption,
15+
private val sessionStore: WCSessionStore,
16+
private val messageLogger: Session.MessageLogger,
17+
transportBuilder: Session.Transport.Builder,
18+
clientMeta: Session.PeerMeta,
19+
clientId: String? = null
1820
) : Session {
1921

2022
private val keyLock = Any()
@@ -91,11 +93,11 @@ class WCSession(
9193
override fun init() {
9294
if (transport.connect()) {
9395
// Register for all messages for this client
94-
transport.send(
95-
Session.Transport.Message(
96-
config.handshakeTopic, "sub", ""
97-
)
96+
val message = Session.Transport.Message(
97+
config.handshakeTopic, "sub", ""
9898
)
99+
transport.send(message)
100+
messageLogger.log(message, isOwnMessage = true)
99101
}
100102
}
101103

@@ -167,11 +169,11 @@ class WCSession(
167169
when (status) {
168170
Session.Transport.Status.Connected -> {
169171
// Register for all messages for this client
170-
transport.send(
171-
Session.Transport.Message(
172-
clientData.id, "sub", ""
173-
)
172+
val message = Session.Transport.Message(
173+
clientData.id, "sub", ""
174174
)
175+
transport.send(message)
176+
messageLogger.log(message, isOwnMessage = true)
175177
}
176178
Session.Transport.Status.Disconnected -> {
177179
// no-op
@@ -194,7 +196,9 @@ class WCSession(
194196
val data: Session.MethodCall
195197
synchronized(keyLock) {
196198
try {
197-
data = payloadAdapter.parse(message.payload, decryptionKey)
199+
val decryptedPayload = payloadEncryption.decrypt(message.payload, decryptionKey)
200+
data = payloadAdapter.parse(decryptedPayload)
201+
messageLogger.log(message.copy(payload = decryptedPayload), isOwnMessage = false)
198202
} catch (e: Exception) {
199203
handlePayloadError(e)
200204
return
@@ -285,13 +289,17 @@ class WCSession(
285289
topic ?: return false
286290

287291
val payload: String
292+
val unencryptedPayload: String
288293
synchronized(keyLock) {
289-
payload = payloadAdapter.prepare(msg, encryptionKey)
294+
unencryptedPayload = payloadAdapter.prepare(msg)
295+
payload = payloadEncryption.encrypt(unencryptedPayload, encryptionKey)
290296
}
291297
callback?.let {
292298
requests[msg.id()] = callback
293299
}
294-
transport.send(Session.Transport.Message(topic, "pub", payload))
300+
val message = Session.Transport.Message(topic, "pub", payload)
301+
transport.send(message)
302+
messageLogger.log(message.copy(payload = unencryptedPayload), isOwnMessage = true)
295303
return true
296304
}
297305

0 commit comments

Comments
 (0)