All Downloads are FREE. Search and download functionalities are using the official Maven repository.

in.svix-kotlin.1.38.0.source-code.Webhook.kt Maven / Gradle / Ivy

package com.svix.kotlin

import com.svix.kotlin.exceptions.WebhookSigningException
import com.svix.kotlin.exceptions.WebhookVerificationException
import java.net.http.HttpHeaders
import java.nio.charset.StandardCharsets
import java.security.InvalidKeyException
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException
import java.util.Base64
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec

class Webhook {
    private val key: ByteArray

    @Throws(WebhookVerificationException::class)
    fun verify(
        payload: String?,
        headers: HttpHeaders,
    ) {
        var msgId = headers.firstValue(SVIX_MSG_ID_KEY)
        var msgSignature = headers.firstValue(SVIX_MSG_SIGNATURE_KEY)
        var msgTimestamp = headers.firstValue(SVIX_MSG_TIMESTAMP_KEY)
        if (msgId.isEmpty || msgSignature.isEmpty || msgTimestamp.isEmpty) {
            // fallback to unbranded
            msgId = headers.firstValue(UNBRANDED_MSG_ID_KEY)
            msgSignature = headers.firstValue(UNBRANDED_MSG_SIGNATURE_KEY)
            msgTimestamp = headers.firstValue(UNBRANDED_MSG_TIMESTAMP_KEY)
            if (msgId.isEmpty || msgSignature.isEmpty || msgTimestamp.isEmpty) {
                throw WebhookVerificationException("Missing required headers")
            }
        }
        val timestamp: Long = verifyTimestamp(msgTimestamp.get())
        val expectedSignature: String =
            try {
                sign(msgId.get(), timestamp, payload).split(",".toRegex()).toTypedArray()[1]
            } catch (e: WebhookSigningException) {
                throw WebhookVerificationException("Failed to generate expected signature")
            }
        val msgSignatures = msgSignature.get().split(" ".toRegex()).toTypedArray()
        for (versionedSignature in msgSignatures) {
            val sigParts = versionedSignature.split(",".toRegex()).toTypedArray()
            if (sigParts.size < 2) {
                continue
            }
            val version = sigParts[0]
            if (version != "v1") {
                continue
            }
            val signature = sigParts[1]
            if (MessageDigest.isEqual(signature.toByteArray(), expectedSignature.toByteArray())) {
                return
            }
        }
        throw WebhookVerificationException("No matching signature found")
    }

    @Throws(WebhookSigningException::class)
    fun sign(
        msgId: String?,
        timestamp: Long,
        payload: String?,
    ): String {
        return try {
            val toSign = String.format("%s.%s.%s", msgId, timestamp, payload)
            val sha512Hmac: Mac = Mac.getInstance(HMAC_SHA256)
            val keySpec = SecretKeySpec(key, HMAC_SHA256)
            sha512Hmac.init(keySpec)
            val macData: ByteArray = sha512Hmac.doFinal(toSign.toByteArray(StandardCharsets.UTF_8))
            val signature = Base64.getEncoder().encodeToString(macData)
            String.format("v1,%s", signature)
        } catch (e: InvalidKeyException) {
            throw WebhookSigningException(e)
        } catch (e: NoSuchAlgorithmException) {
            throw WebhookSigningException(e)
        }
    }

    companion object {
        const val SECRET_PREFIX = "whsec_"
        const val SVIX_MSG_ID_KEY = "svix-id"
        const val SVIX_MSG_SIGNATURE_KEY = "svix-signature"
        const val SVIX_MSG_TIMESTAMP_KEY = "svix-timestamp"
        const val UNBRANDED_MSG_ID_KEY = "webhook-id"
        const val UNBRANDED_MSG_SIGNATURE_KEY = "webhook-signature"
        const val UNBRANDED_MSG_TIMESTAMP_KEY = "webhook-timestamp"
        private const val HMAC_SHA256 = "HmacSHA256"
        private const val TOLERANCE_IN_SECONDS = 5 * 60 // 5 minutes
        private const val SECOND_IN_MS = 1000L

        @Throws(WebhookVerificationException::class)
        private fun verifyTimestamp(timestampHeader: String): Long {
            val now: Long = System.currentTimeMillis() / SECOND_IN_MS
            val timestamp: Long =
                try {
                    timestampHeader.toLong()
                } catch (e: NumberFormatException) {
                    throw WebhookVerificationException("Invalid Signature Headers")
                }

            if (timestamp < now - TOLERANCE_IN_SECONDS) {
                throw WebhookVerificationException("Message timestamp too old")
            }
            if (timestamp > now + TOLERANCE_IN_SECONDS) {
                throw WebhookVerificationException("Message timestamp too new")
            }
            return timestamp
        }
    }

    constructor(secret: String) {
        var sec = secret
        if (sec.startsWith(SECRET_PREFIX)) {
            sec = sec.substring(SECRET_PREFIX.length)
        }
        key = Base64.getDecoder().decode(sec)
    }

    constructor(secret: ByteArray) {
        key = secret
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy