Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package co.nilin.opex.api.core.inout

object DepositWebhookHeaders {
const val SIGNATURE = "X-Signature"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package co.nilin.opex.api.core.inout

import java.math.BigDecimal

data class DepositWebhookRequest(
val referenceNumber: String,
val depositNumber: String,
val symbol: String,
val amount: BigDecimal,
val externalIdentifier: String,
val date: Long
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package co.nilin.opex.api.core.inout

data class DepositWebhookResponse(
val referenceNumber: String? = null,
val status: String,
val duplicate: Boolean = false,
val message: String? = null
)

Original file line number Diff line number Diff line change
Expand Up @@ -261,5 +261,5 @@ interface WalletProxy {

suspend fun deleteGateway(token: String, gatewayUuid: String, currencySymbol: String)


suspend fun submitDepositWebhook(request: DepositWebhookRequest, signature: String): DepositWebhookResponse
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ class SecurityConfig(
.pathMatchers(HttpMethod.DELETE, "/v3/order").hasAuthority("PERM_order:write")

// Opex endpoints
.pathMatchers("/v1/deposit/webhook").permitAll()
.pathMatchers("/opex/v1/admin/transactions/**").hasAnyAuthority("ROLE_monitoring", "ROLE_admin")
.pathMatchers("/opex/v1/storage/**").permitAll()
.pathMatchers("/opex/v1/web/config/**").permitAll()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package co.nilin.opex.api.ports.opex.controller

import co.nilin.opex.api.core.inout.DepositWebhookRequest
import co.nilin.opex.api.core.inout.DepositWebhookResponse
import co.nilin.opex.api.core.inout.DepositWebhookHeaders
import co.nilin.opex.api.core.spi.WalletProxy
import org.springframework.web.bind.annotation.*

@RestController
@RequestMapping("/v1/deposit")
class DepositWebhookController(
private val walletProxy: WalletProxy
) {
@PostMapping(
"/webhook"
)
suspend fun submit(
@RequestHeader(DepositWebhookHeaders.SIGNATURE) sign: String, @RequestBody body: DepositWebhookRequest
): DepositWebhookResponse {
return walletProxy.submitDepositWebhook(
body,
sign
)
}
}


Original file line number Diff line number Diff line change
Expand Up @@ -1110,5 +1110,23 @@ class WalletProxyImpl(@Qualifier("generalWebClient") private val webClient: WebC
}
.awaitBodilessEntity()
}
override suspend fun submitDepositWebhook(
request: DepositWebhookRequest,
signature: String
): DepositWebhookResponse {
logger.info("proxying deposit webhook to wallet")

return withContext(ProxyDispatchers.wallet) {
webClient.post()
.uri("$baseUrl/v1/deposit/webhook")
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON)
.header(DepositWebhookHeaders.SIGNATURE, signature)
.bodyValue(request)
.retrieve()
.bodyToMono<DepositWebhookResponse>()
.awaitSingle()
}
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -48,20 +48,5 @@ class AppConfig(private val resourceLoader: ResourceLoader) {
adminKafkaEventListener.addEventListener(adminEventListener)
}

@Bean("webhookPublicKey")
fun webhookPublicKey(): PublicKey {
val publicKeyString = resourceLoader.getResource("classpath:scanner-public.pem").inputStream
.readAllBytes()
.toString(Charsets.UTF_8)
.replace("-----BEGIN PUBLIC KEY-----", "")
.replace("-----END PUBLIC KEY-----", "")
.replace("\n", "")
.replace("\r", "")

val keyBytes = Base64.getDecoder().decode(publicKeyString)
val keySpec = X509EncodedKeySpec(keyBytes)
val keyFactory = KeyFactory.getInstance("RSA")
return keyFactory.generatePublic(keySpec)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,12 @@ package co.nilin.opex.bcgateway.app.controller
import co.nilin.opex.bcgateway.core.api.WalletSyncService
import co.nilin.opex.bcgateway.core.model.Transfer
import co.nilin.opex.bcgateway.core.model.Wallet
import co.nilin.opex.common.OpexError
import co.nilin.opex.utility.error.data.OpexException
import co.nilin.opex.common.utils.SignVerifier
import com.fasterxml.jackson.databind.ObjectMapper
import org.slf4j.LoggerFactory
import org.springframework.core.io.ResourceLoader
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestHeader
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.bind.annotation.*
import java.math.BigDecimal
import java.security.PublicKey
import java.security.Signature
import java.util.*

data class WebhookBody(
val txId: String,
Expand All @@ -33,7 +25,7 @@ data class WebhookBody(
@RestController
@RequestMapping("/scanner")
class ScannerController(
private val publicKey: PublicKey,
private val resourceLoader: ResourceLoader,
private val mapper: ObjectMapper,
private val service: WalletSyncService
) {
Expand All @@ -42,30 +34,13 @@ class ScannerController(

@PostMapping("/webhook")
suspend fun webhook(@RequestHeader("X-Signature") sign: String, @RequestBody body: WebhookBody) {
verifySignature(sign, body)
val publicKeyRawStr = resourceLoader.getResource("classpath:scanner-public.pem").inputStream
.readAllBytes()
.toString(Charsets.UTF_8)
SignVerifier().verify("SHA256withRSA", publicKeyRawStr, mapper.writeValueAsString(body), sign)
logger.info("Webhook received for address ${body.address}, amount ${body.amount}")
service.sendTransfer(with(body) { Transfer(txId, Wallet(address, memo), isToken, amount, chain, tokenAddress) })
}

private fun verifySignature(sign: String, request: WebhookBody) {
try {
logger.info("Verifying signature for address ${request.address}")
val reqStr = mapper.writeValueAsString(request)
val decodedSign = Base64.getDecoder().decode(sign)
val verifier = Signature.getInstance("SHA256withRSA").apply {
initVerify(publicKey)
update(reqStr.toByteArray())
}

if (!verifier.verify(decodedSign)) {
logger.warn("Signature is not valid!")
throw OpexError.Forbidden.exception()
}
} catch (e: OpexException) {
throw e
} catch (e: Exception) {
logger.error("Unable to verify signature", e)
throw OpexError.InternalServerError.exception()
}
}
}
58 changes: 58 additions & 0 deletions common/src/main/kotlin/co/nilin/opex/common/utils/SignVerifier.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package co.nilin.opex.common.utils


import org.springframework.http.HttpStatus
import org.springframework.web.server.ResponseStatusException
import java.security.KeyFactory
import java.security.Signature
import java.security.spec.X509EncodedKeySpec
import java.util.Base64

class SignVerifier{

fun verify(signatureAlgorithm: String , publicKeyPem: String, payload: String, signatureBase64: String) {
val signatureBytes = try {
Base64.getDecoder().decode(signatureBase64)
} catch (ex: Exception) {
throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid fiat scanner signature encoding")
}

val publicKey = try {
val normalizedPem = publicKeyPem
.replace("-----BEGIN PUBLIC KEY-----", "")
.replace("-----END PUBLIC KEY-----", "")
.replace("\\s".toRegex(), "")

val keyBytes = Base64.getDecoder().decode(normalizedPem)
val keySpec = X509EncodedKeySpec(keyBytes)

KeyFactory
.getInstance(resolveKeyFactoryAlgorithm(signatureAlgorithm))
.generatePublic(keySpec)
} catch (ex: Exception) {
throw ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Invalid fiat scanner public key")
}

val verified = try {
Signature.getInstance(signatureAlgorithm).apply {
initVerify(publicKey)
update(payload.toByteArray(Charsets.UTF_8))
}.verify(signatureBytes)
} catch (ex: Exception) {
throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Fiat scanner signature verification failed")
}

if (!verified) {
throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid fiat scanner signature")
}
}

private fun resolveKeyFactoryAlgorithm(algorithm: String): String {
return when {
algorithm.equals("Ed25519", ignoreCase = true) -> "Ed25519"
algorithm.contains("RSA", ignoreCase = true) -> "RSA"
algorithm.contains("ECDSA", ignoreCase = true) -> "EC"
else -> "RSA"
}
}
}
2 changes: 1 addition & 1 deletion device-management/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
<kotlin.version>2.1.0</kotlin.version>
<spring.version>3.4.2</spring.version>
<spring-cloud.version>2024.0.0</spring-cloud.version>
<error-hanlder.version>1.2.19</error-hanlder.version>
<error-hanlder.version>1.2.25</error-hanlder.version>
</properties>


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,8 @@ class ProfileManagement(
ProfileUpdatedEvent(
userId = userId,
firstName = request.firstName,
lastName = request.lastName
lastName = request.lastName,
identifier = request.identifier
)
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ data class ProfileUpdatedEvent(
var firstName: String? = null,
var lastName: String? = null,
var email: String? = null,
var mobile: String? = null
var mobile: String? = null,
var identifier: String? = null
)
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class SecurityConfig(private val webClient: WebClient) {
fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain? {
http.csrf().disable()
.authorizeExchange()
.pathMatchers("/v1/deposit/webhook").permitAll()
.pathMatchers("/balanceOf/**").authenticated()
.pathMatchers("/owner/**").authenticated()
.pathMatchers("/withdraw").authenticated()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
package co.nilin.opex.wallet.app.controller


import co.nilin.opex.common.utils.SignVerifier
import co.nilin.opex.wallet.app.dto.DepositHistoryRequest
import co.nilin.opex.wallet.app.service.DepositService
import co.nilin.opex.wallet.app.utils.asLocalDateTime
import co.nilin.opex.wallet.core.inout.DepositResponse
import co.nilin.opex.wallet.core.inout.TransactionSummary
import co.nilin.opex.wallet.core.inout.TransferResult
import co.nilin.opex.wallet.core.inout.*
import co.nilin.opex.wallet.core.model.DepositType
import co.nilin.opex.wallet.core.model.WalletType
import com.fasterxml.jackson.databind.ObjectMapper
import io.swagger.annotations.ApiResponse
import io.swagger.annotations.Example
import io.swagger.annotations.ExampleProperty
import org.springframework.core.io.ResourceLoader
import org.springframework.security.core.annotation.CurrentSecurityContext
import org.springframework.security.core.context.SecurityContext
import org.springframework.web.bind.annotation.*
Expand All @@ -24,6 +25,8 @@ import java.time.ZoneId
@RequestMapping
class DepositController(
private val depositService: DepositService,
private val resourceLoader: ResourceLoader,
private val mapper: ObjectMapper,
) {

@PostMapping("/v1/deposit/history")
Expand Down Expand Up @@ -114,6 +117,22 @@ class DepositController(
limit,
)
}

@PostMapping("/v1/deposit/webhook")
suspend fun submit(
@RequestBody request: DepositWebhookRequest,
@RequestHeader(DepositWebhookHeaders.SIGNATURE) signature: String
): DepositWebhookResponse {
val publicKeyRawStr = resourceLoader.getResource("classpath:scanner-public.pem").inputStream
.readAllBytes()
.toString(Charsets.UTF_8)
SignVerifier().verify("SHA512withRSA", publicKeyRawStr, mapper.writeValueAsString(request), signature)
return depositService.processExternalDeposit(request)
}

object DepositWebhookHeaders {
const val SIGNATURE = "X-Signature"
}
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ class WalletOwnerController(
val owner = walletOwnerManager.findWalletOwner(uuid) ?: run {
if (currentUserProvider.getCurrentUser()?.uuid.equals(uuid) && environment.activeProfiles.contains("otc"))
walletOwnerManager.createWalletOwner(
uuid, currentUserProvider.getCurrentUser()?.mobile ?: "not set", ""
uuid, currentUserProvider.getCurrentUser()?.mobile ?: "not set", "", currentUserProvider.getCurrentUser()?.identityId
)
throw OpexError.WalletOwnerNotFound.exception()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ data class CurrentUser(
val fullName: String?,
val mobile: String?,
val roles: List<String>,
val level: String?
val level: String?,
val identityId: String?
)
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@ class ProfileUpdatedEventListenerImpl(private val walletOwnerManager: WalletOwne
if (!event.firstName.isNullOrBlank() && !event.lastName.isNullOrBlank()) {
runBlocking {
logger.info("Incoming ProfileUpdated event $event")
walletOwnerManager.updateWalletOwnerName(event.userId, "${event.firstName} ${event.lastName}")

walletOwnerManager.updateWalletOwnerName(event.userId, "${event.firstName} ${event.lastName}", event.identifier)
}
} else Unit
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ class CurrentUserProvider {
).joinToString(" "),
mobile = jwt.getClaimAsString("mobile"),
roles = jwt.getClaimAsStringList("roles") ?: emptyList(),
level = jwt.getClaimAsString("level")
level = jwt.getClaimAsString("level"),
identityId = jwt.getClaimAsString("identity_id")
)
}
}
Expand Down
Loading
Loading