diff --git a/lightspark-sdk/README.md b/lightspark-sdk/README.md index 308e846a..ead97139 100644 --- a/lightspark-sdk/README.md +++ b/lightspark-sdk/README.md @@ -17,14 +17,14 @@ Start by installing the SDK from maven: **build.gradle:** ```groovy dependencies { - implementation "com.lightspark:lightspark-sdk:0.20.0" + implementation "com.lightspark:lightspark-sdk:0.20.1" } ``` or with **build.gradle.kts:** ```kotlin dependencies { - implementation("com.lightspark:lightspark-sdk:0.20.0") + implementation("com.lightspark:lightspark-sdk:0.20.1") } ``` diff --git a/lightspark-sdk/gradle.properties b/lightspark-sdk/gradle.properties index 93bbdec8..48c2d8e0 100644 --- a/lightspark-sdk/gradle.properties +++ b/lightspark-sdk/gradle.properties @@ -1,7 +1,7 @@ GROUP=com.lightspark POM_ARTIFACT_ID=lightspark-sdk # Don't bump this manually. Run `scripts/versions.main.kt ` to bump the version instead. -VERSION_NAME=0.20.0 +VERSION_NAME=0.20.1 POM_DESCRIPTION=The Lightspark API SDK for Kotlin and Java. POM_INCEPTION_YEAR=2023 diff --git a/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/webhooks/Webhooks.kt b/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/webhooks/Webhooks.kt index 1c4e6f39..cfb74228 100644 --- a/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/webhooks/Webhooks.kt +++ b/lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/webhooks/Webhooks.kt @@ -5,6 +5,7 @@ package com.lightspark.sdk.webhooks import com.lightspark.sdk.core.LightsparkException import com.lightspark.sdk.model.WebhookEventType import com.lightspark.sdk.util.serializerFormat +import java.security.MessageDigest import javax.crypto.Mac import javax.crypto.spec.SecretKeySpec import kotlinx.datetime.Instant @@ -39,18 +40,24 @@ const val SIGNATURE_HEADER = "lightspark-signature" @OptIn(ExperimentalStdlibApi::class) @Throws(LightsparkException::class) -fun verifyAndParseWebhook( - data: ByteArray, - hexDigest: String, - webhookSecret: String, -): WebhookEvent { +fun verifyAndParseWebhook(data: ByteArray, hexDigest: String, webhookSecret: String): WebhookEvent { val hmac = Mac.getInstance("HmacSHA256") val secretKey = SecretKeySpec(webhookSecret.encodeToByteArray(), "HmacSHA256") hmac.init(secretKey) hmac.update(data) val signature = hmac.doFinal() - val verified = signature.contentEquals(hexDigest.hexToByteArray()) - if (!verified) { + + val digestBytes = + try { + hexDigest.hexToByteArray() + } catch (_ : IllegalArgumentException) { + throw LightsparkException( + "Webhook signature verification failed. Invalid message signature format.", + "webhook_signature_verification_failed", + ) + } + + if (!MessageDigest.isEqual(signature, digestBytes)) { throw LightsparkException("Webhook signature verification failed", "webhook_signature_verification_failed") } return parseWebhook(data) diff --git a/lightspark-sdk/src/commonTest/kotlin/com/lightspark/sdk/webhooks/WebhookTests.kt b/lightspark-sdk/src/commonTest/kotlin/com/lightspark/sdk/webhooks/WebhookTests.kt index dc3d08b1..70f222e7 100644 --- a/lightspark-sdk/src/commonTest/kotlin/com/lightspark/sdk/webhooks/WebhookTests.kt +++ b/lightspark-sdk/src/commonTest/kotlin/com/lightspark/sdk/webhooks/WebhookTests.kt @@ -3,14 +3,15 @@ package com.lightspark.sdk.webhooks import com.lightspark.sdk.core.LightsparkException import com.lightspark.sdk.model.WebhookEventType import kotlin.test.Test +import kotlin.test.assertContains import kotlin.test.assertEquals -import kotlin.test.assertNotNull +import kotlin.test.assertFailsWith import kotlin.test.assertNull -import kotlin.test.assertTrue -import kotlin.test.fail import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import kotlinx.datetime.Instant +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.MissingFieldException import kotlinx.serialization.json.jsonPrimitive @OptIn(ExperimentalCoroutinesApi::class) @@ -18,18 +19,15 @@ class WebhookTests { @Test fun `test valid verifyAndParse`() = runTest { val eventType = WebhookEventType.NODE_STATUS - val eventId = "1615c8be5aa44e429eba700db2ed8ca5" - val entityId = "lightning_node:01882c25-157a-f96b-0000-362d42b64397" - val timestamp = Instant.parse("2023-05-17T23:56:47.874449+00:00") val data = """{"event_type": "NODE_STATUS", "event_id": "1615c8be5aa44e429eba700db2ed8ca5", "timestamp": "2023-05-17T23:56:47.874449+00:00", "entity_id": "lightning_node:01882c25-157a-f96b-0000-362d42b64397"}""" - val hexdigest = "62a8829aeb48b4142533520b1f7f86cdb1ee7d718bf3ea15bc1c662d4c453b74" + val hexDigest = "62a8829aeb48b4142533520b1f7f86cdb1ee7d718bf3ea15bc1c662d4c453b74" val webhookSecret = "3gZ5oQQUASYmqQNuEk0KambNMVkOADDItIJjzUlAWjX" - val event = verifyAndParseWebhook(data.encodeToByteArray(), hexdigest, webhookSecret) + val event = verifyAndParseWebhook(data.encodeToByteArray(), hexDigest, webhookSecret) assertEquals(eventType, event.eventType) - assertEquals(eventId, event.eventId) - assertEquals(entityId, event.entityId) - assertEquals(timestamp, event.timestamp) + assertEquals("1615c8be5aa44e429eba700db2ed8ca5", event.eventId) + assertEquals("lightning_node:01882c25-157a-f96b-0000-362d42b64397", event.entityId) + assertEquals(Instant.parse("2023-05-17T23:56:47.874449+00:00"), event.timestamp) assertNull(event.walletId) assertNull(event.data) } @@ -37,48 +35,53 @@ class WebhookTests { @Test fun `test invalid signature`() = runTest { val data = """{"event_type": "NODE_STATUS", "event_id": "1615c8be5aa44e429eba700db2ed8ca5", "timestamp": "2023-05-17T23:56:47.874449+00:00", "entity_id": "lightning_node:01882c25-157a-f96b-0000-362d42b64397"}""" - val hexdigest = "deadbeef" + val hexDigest = "deadbeef" val webhookSecret = "3gZ5oQQUASYmqQNuEk0KambNMVkOADDItIJjzUlAWjX" - try { - verifyAndParseWebhook(data.encodeToByteArray(), hexdigest, webhookSecret) - fail("Expected LightsparkException") - } catch (e: LightsparkException) { - assertEquals("webhook_signature_verification_failed", e.errorCode) - } + val err = assertFailsWith { + verifyAndParseWebhook(data.encodeToByteArray(), hexDigest, webhookSecret) + } + assertEquals("webhook_signature_verification_failed", err.errorCode) + } + + @Test + fun `test invalid digest bytes`() = runTest { + val data = """{"event_type": "NODE_STATUS", "event_id": "1615c8be5aa44e429eba700db2ed8ca5", "timestamp": "2023-05-17T23:56:47.874449+00:00", "entity_id": "lightning_node:01882c25-157a-f96b-0000-362d42b64397"}""" + val webhookSecret = "3gZ5oQQUASYmqQNuEk0KambNMVkOADDItIJjzUlAWjX" + val hexDigest = "NotAHexDigest" + + val err = assertFailsWith { + verifyAndParseWebhook(data.encodeToByteArray(), hexDigest, webhookSecret) + } + assertEquals("webhook_signature_verification_failed", err.errorCode) } @Test + @OptIn(ExperimentalSerializationApi::class) fun `test invalid json structure`() = runTest { val data = """{"event_typeeee": "NODE_STATUS", "event_id": "1615c8be5aa44e429eba700db2ed8ca5", "timestamp": "2023-05-17T23:56:47.874449+00:00", "entity_id": "lightning_node:01882c25-157a-f96b-0000-362d42b64397"}""" - val hexdigest = "4c4232ea3cccf8d40f56f873ef3a353ad8c80f2e6ea3404197d08c4d46274bf4" + val hexDigest = "4c4232ea3cccf8d40f56f873ef3a353ad8c80f2e6ea3404197d08c4d46274bf4" val webhookSecret = "3gZ5oQQUASYmqQNuEk0KambNMVkOADDItIJjzUlAWjX" - try { - verifyAndParseWebhook(data.encodeToByteArray(), hexdigest, webhookSecret) - fail("Expected Exception") - } catch (e: Exception) { - assertTrue(e.message!!.contains("event_type")) + val err = assertFailsWith { + verifyAndParseWebhook(data.encodeToByteArray(), hexDigest, webhookSecret) } + assertContains(err.message!!, "event_type") } @Test fun `test valid verifyAndParse with wallet`() = runTest { val eventType = WebhookEventType.WALLET_INCOMING_PAYMENT_FINISHED - val eventId = "1615c8be5aa44e429eba700db2ed8ca5" - val entityId = "lightning_node:01882c25-157a-f96b-0000-362d42b64397" - val walletId = "wallet:01882c25-157a-f96b-0000-362d42b64397" - val timestamp = Instant.parse("2023-05-17T23:56:47.874449+00:00") val data = """{"event_type": "WALLET_INCOMING_PAYMENT_FINISHED", "event_id": "1615c8be5aa44e429eba700db2ed8ca5", "timestamp": "2023-05-17T23:56:47.874449+00:00", "entity_id": "lightning_node:01882c25-157a-f96b-0000-362d42b64397", "wallet_id": "wallet:01882c25-157a-f96b-0000-362d42b64397" }""" - val hexdigest = "b4eeb95f18956b3c33b99e9effc61636effc4634f83604cb41de13470c42669a" + val hexDigest = "b4eeb95f18956b3c33b99e9effc61636effc4634f83604cb41de13470c42669a" val webhookSecret = "3gZ5oQQUASYmqQNuEk0KambNMVkOADDItIJjzUlAWjX" - val event = verifyAndParseWebhook(data.encodeToByteArray(), hexdigest, webhookSecret) + val event = verifyAndParseWebhook(data.encodeToByteArray(), hexDigest, webhookSecret) assertEquals(eventType, event.eventType) - assertEquals(eventId, event.eventId) - assertEquals(entityId, event.entityId) - assertEquals(walletId, event.walletId) - assertEquals(timestamp, event.timestamp) + assertEquals("1615c8be5aa44e429eba700db2ed8ca5", event.eventId) + assertEquals("lightning_node:01882c25-157a-f96b-0000-362d42b64397", event.entityId) + assertEquals("wallet:01882c25-157a-f96b-0000-362d42b64397", event.walletId) + assertEquals(Instant.parse("2023-05-17T23:56:47.874449+00:00"), event.timestamp) assertNull(event.data) } @@ -87,7 +90,6 @@ class WebhookTests { val data = """{"event_type": "REMOTE_SIGNING", "event_id": "8be9c360a68e420b9126b43ff6007a32", "timestamp": "2023-08-10T02:14:27.559234+00:00", "entity_id": "node_with_server_signing:0189d6bc-558d-88df-0000-502f04e71816", "data": {"sub_event_type": "GET_PER_COMMITMENT_POINT", "bitcoin_network": "TESTNET", "derivation_path": "m/3/2104864975", "per_commitment_point_idx": 281474976710654}}""" val event = parseWebhook(data.encodeToByteArray()) assertEquals(WebhookEventType.REMOTE_SIGNING, event.eventType) - assertNotNull(event.data) assertEquals("GET_PER_COMMITMENT_POINT", event.data!!["sub_event_type"]?.jsonPrimitive?.content) } } diff --git a/remotesignerdemo/src/main/kotlin/com/lightspark/WebhookHandler.kt b/remotesignerdemo/src/main/kotlin/com/lightspark/WebhookHandler.kt index d8fde762..933e5f3b 100644 --- a/remotesignerdemo/src/main/kotlin/com/lightspark/WebhookHandler.kt +++ b/remotesignerdemo/src/main/kotlin/com/lightspark/WebhookHandler.kt @@ -22,7 +22,7 @@ suspend fun handleWebhookRequest( val webhookEvent = try { val bodyBytes = call.receiveText().toByteArray() verifyAndParseWebhook(bodyBytes, signature, config.webhookSecret) - } catch (e: Exception) { + } catch (_: Exception) { call.respond(HttpStatusCode.BadRequest, "Invalid webhook request.") return "Invalid webhook request or bad signature." } @@ -30,7 +30,7 @@ suspend fun handleWebhookRequest( val response = when (webhookEvent.eventType) { WebhookEventType.REMOTE_SIGNING -> try { handleRemoteSigningEvent(client, webhookEvent, config.masterSeed) - } catch (e: Exception) { + } catch (_: Exception) { call.respond(HttpStatusCode.InternalServerError, "Error handling remote signing event.") return "Error handling remote signing event." }