summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAntoine A <>2024-02-25 19:06:50 +0100
committerAntoine A <>2024-02-25 19:06:50 +0100
commit0067499c54fc60784f2cf2ec9be8340da7a5459e (patch)
treeb4a6029887d25859e7ebaa1b1960986981c8458f
parentb421a7b263d97ade06c7e87dda543d4b7706f59d (diff)
downloadlibeufin-0067499c54fc60784f2cf2ec9be8340da7a5459e.tar.gz
libeufin-0067499c54fc60784f2cf2ec9be8340da7a5459e.tar.bz2
libeufin-0067499c54fc60784f2cf2ec9be8340da7a5459e.zip
Fix nexus failing to parse wellformed incoming transactions subject and share more code between nexus and bank
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt15
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/TalerCommon.kt119
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt4
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/db/ExchangeDAO.kt7
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/db/TransactionDAO.kt36
-rw-r--r--bank/src/test/kotlin/BankIntegrationApiTest.kt13
-rw-r--r--bank/src/test/kotlin/CoreBankApiTest.kt14
-rw-r--r--bank/src/test/kotlin/DatabaseTest.kt2
-rw-r--r--bank/src/test/kotlin/StatsTest.kt7
-rw-r--r--bank/src/test/kotlin/WireGatewayApiTest.kt30
-rw-r--r--bank/src/test/kotlin/helpers.kt22
-rw-r--r--common/src/main/kotlin/CryptoUtil.kt9
-rw-r--r--common/src/main/kotlin/TalerCommon.kt121
-rw-r--r--common/src/main/kotlin/TxMedatada.kt (renamed from bank/src/main/kotlin/tech/libeufin/bank/Metadata.kt)15
-rw-r--r--common/src/main/kotlin/random.kt28
-rw-r--r--common/src/test/kotlin/CryptoUtilTest.kt8
-rwxr-xr-xcontrib/ci/jobs/0-codespell/job.sh1
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/Database.kt8
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/EbicsFetch.kt100
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022.kt2
-rw-r--r--nexus/src/test/kotlin/DatabaseTest.kt5
-rw-r--r--nexus/src/test/kotlin/Parsing.kt58
-rw-r--r--testbench/src/main/kotlin/Main.kt6
-rw-r--r--testbench/src/test/kotlin/IntegrationTest.kt22
24 files changed, 278 insertions, 374 deletions
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt b/bank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt
index dbb550ac..c2efaf76 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt
@@ -43,7 +43,6 @@ import java.time.Duration
import java.time.Instant
import java.time.temporal.ChronoUnit
import java.util.*
-import kotlin.random.Random
private val logger: Logger = LoggerFactory.getLogger("libeufin-bank-api")
@@ -96,7 +95,7 @@ private fun Routing.coreBankTokenApi(db: Database) {
TalerErrorCode.GENERIC_TOKEN_PERMISSION_INSUFFICIENT
)
}
- val tokenBytes = ByteArray(32).apply { Random.nextBytes(this) }
+ val token = Base32Crockford32B.rand()
val tokenDuration: Duration = req.duration?.d_us ?: TOKEN_DEFAULT_DURATION
val creationTime = Instant.now()
@@ -114,7 +113,7 @@ private fun Routing.coreBankTokenApi(db: Database) {
}
if (!db.token.create(
login = username,
- content = tokenBytes,
+ content = token.raw,
creationTime = creationTime,
expirationTime = expirationTimestamp,
scope = req.scope,
@@ -124,7 +123,7 @@ private fun Routing.coreBankTokenApi(db: Database) {
}
call.respond(
TokenSuccessResponse(
- access_token = Base32Crockford.encode(tokenBytes),
+ access_token = token.encoded(),
expiration = TalerProtocolTimestamp(t_s = expirationTimestamp)
)
)
@@ -677,9 +676,13 @@ private fun Routing.coreBankTanApi(db: Database, ctx: BankConfig) {
} catch (e: Exception) {
process.destroy()
}
- val exitValue = process.exitValue()
+ val exitValue = process.exitValue()
if (exitValue != 0) {
- val out = process.getInputStream().reader().readText()
+ val out = runCatching {
+ process.getInputStream().use {
+ reader().readText()
+ }
+ }.getOrDefault("")
if (out.isNotEmpty()) {
logger.error("TAN ${res.tanChannel} - ${tanScript}: $out")
}
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/TalerCommon.kt b/bank/src/main/kotlin/tech/libeufin/bank/TalerCommon.kt
index de0ebc20..93d649ee 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/TalerCommon.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/TalerCommon.kt
@@ -1,6 +1,6 @@
/*
* This file is part of LibEuFin.
- * Copyright (C) 2023 Taler Systems S.A.
+ * Copyright (C) 2023-2024 Taler Systems S.A.
* LibEuFin is free software; you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
@@ -39,123 +39,6 @@ import java.time.Instant
import java.time.temporal.ChronoUnit
import java.util.concurrent.TimeUnit
-/** 32-byte Crockford's Base32 encoded data */
-@Serializable(with = Base32Crockford32B.Serializer::class)
-class Base32Crockford32B {
- private var encoded: String? = null
- val raw: ByteArray
-
- constructor(encoded: String) {
- val decoded = try {
- Base32Crockford.decode(encoded)
- } catch (e: EncodingException) {
- null
- }
-
- require(decoded != null) {
- "Data should be encoded using Crockford's Base32"
- }
- require(decoded.size == 32) {
- "Encoded data should be 32 bytes long"
- }
- this.raw = decoded
- this.encoded = encoded
- }
- constructor(raw: ByteArray) {
- require(raw.size == 32) {
- "Encoded data should be 32 bytes long"
- }
- this.raw = raw
- }
-
- fun encoded(): String {
- encoded = encoded ?: Base32Crockford.encode(raw)
- return encoded!!
- }
-
- override fun toString(): String {
- return encoded()
- }
-
- override fun equals(other: Any?) = (other is Base32Crockford32B) && raw.contentEquals(other.raw)
-
- internal object Serializer : KSerializer<Base32Crockford32B> {
- override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Base32Crockford32B", PrimitiveKind.STRING)
-
- override fun serialize(encoder: Encoder, value: Base32Crockford32B) {
- encoder.encodeString(value.encoded())
- }
-
- override fun deserialize(decoder: Decoder): Base32Crockford32B {
- return Base32Crockford32B(decoder.decodeString())
- }
- }
-}
-
-/** 64-byte Crockford's Base32 encoded data */
-@Serializable(with = Base32Crockford64B.Serializer::class)
-class Base32Crockford64B {
- private var encoded: String? = null
- val raw: ByteArray
-
- constructor(encoded: String) {
- val decoded = try {
- Base32Crockford.decode(encoded)
- } catch (e: EncodingException) {
- null
- }
-
- require(decoded != null) {
- "Data should be encoded using Crockford's Base32"
- }
- require(decoded.size == 64) {
- "Encoded data should be 32 bytes long"
- }
- this.raw = decoded
- this.encoded = encoded
- }
- constructor(raw: ByteArray) {
- require(raw.size == 64) {
- "Encoded data should be 32 bytes long"
- }
- this.raw = raw
- }
-
- fun encoded(): String {
- encoded = encoded ?: Base32Crockford.encode(raw)
- return encoded!!
- }
-
- override fun toString(): String {
- return encoded()
- }
-
- override fun equals(other: Any?) = (other is Base32Crockford64B) && raw.contentEquals(other.raw)
-
- internal object Serializer : KSerializer<Base32Crockford64B> {
- override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Base32Crockford64B", PrimitiveKind.STRING)
-
- override fun serialize(encoder: Encoder, value: Base32Crockford64B) {
- encoder.encodeString(value.encoded())
- }
-
- override fun deserialize(decoder: Decoder): Base32Crockford64B {
- return Base32Crockford64B(decoder.decodeString())
- }
- }
-}
-
-/** 32-byte hash code */
-typealias ShortHashCode = Base32Crockford32B
-/** 64-byte hash code */
-typealias HashCode = Base32Crockford64B
-/**
- * EdDSA and ECDHE public keys always point on Curve25519
- * and represented using the standard 256 bits Ed25519 compact format,
- * converted to Crockford Base32.
- */
-typealias EddsaPublicKey = Base32Crockford32B
-
/** Timestamp containing the number of seconds since epoch */
@Serializable
data class TalerProtocolTimestamp(
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt b/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt
index 5f98ae07..facc26d6 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt
@@ -26,9 +26,7 @@ import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
-import tech.libeufin.common.IbanPayto
-import tech.libeufin.common.Payto
-import tech.libeufin.common.TalerAmount
+import tech.libeufin.common.*
import java.time.Instant
/**
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/ExchangeDAO.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/ExchangeDAO.kt
index af4639df..6540172a 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/db/ExchangeDAO.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/db/ExchangeDAO.kt
@@ -1,6 +1,6 @@
/*
* This file is part of LibEuFin.
- * Copyright (C) 2023 Taler Systems S.A.
+ * Copyright (C) 2023-2024 Taler Systems S.A.
* LibEuFin is free software; you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
@@ -20,10 +20,7 @@
package tech.libeufin.bank.db
import tech.libeufin.bank.*
-import tech.libeufin.common.BankPaytoCtx
-import tech.libeufin.common.getAmount
-import tech.libeufin.common.getBankPayto
-import tech.libeufin.common.toDbMicros
+import tech.libeufin.common.*
import java.time.Instant
/** Data access logic for exchange specific logic */
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/TransactionDAO.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/TransactionDAO.kt
index be427b66..9b03cd2d 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/db/TransactionDAO.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/db/TransactionDAO.kt
@@ -1,6 +1,6 @@
/*
* This file is part of LibEuFin.
- * Copyright (C) 2023 Taler Systems S.A.
+ * Copyright (C) 2023-2024 Taler Systems S.A.
* LibEuFin is free software; you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
@@ -94,23 +94,25 @@ class TransactionDAO(private val db: Database) {
if (exchangeCreditor && exchangeDebtor) {
logger.warn("exchange account $exchangeDebtor sent a manual transaction to exchange account $exchangeCreditor, this should never happens and is not bounced to prevent bouncing loop, may fail in the future")
} else if (exchangeCreditor) {
- val reservePub = parseIncomingTxMetadata(subject)
- val bounceCause = if (reservePub != null) {
- val registered = conn.prepareStatement("CALL register_incoming(?, ?)").run {
- setBytes(1, reservePub.raw)
- setLong(2, creditRowId)
- executeProcedureViolation()
+ val bounceCause = runCatching { parseIncomingTxMetadata(subject) }.fold(
+ onSuccess = { reservePub ->
+ val registered = conn.prepareStatement("CALL register_incoming(?, ?)").run {
+ setBytes(1, reservePub.raw)
+ setLong(2, creditRowId)
+ executeProcedureViolation()
+ }
+ if (!registered) {
+ logger.warn("exchange account $creditAccountId received an incoming taler transaction $creditRowId with an already used reserve public key")
+ "reserve public key reuse"
+ } else {
+ null
+ }
+ },
+ onFailure = { e ->
+ logger.warn("exchange account $creditAccountId received a manual transaction $creditRowId with malformed metadata: ${e.message}")
+ "malformed metadata: ${e.message}"
}
- if (!registered) {
- logger.warn("exchange account $creditAccountId received an incoming taler transaction $creditRowId with an already used reserve public key")
- "reserve public key reuse"
- } else {
- null
- }
- } else {
- logger.warn("exchange account $creditAccountId received a manual transaction $creditRowId with malformed metadata")
- "malformed metadata"
- }
+ )
if (bounceCause != null) {
// No error can happens because an opposite transaction already took place in the same transaction
conn.prepareStatement("""
diff --git a/bank/src/test/kotlin/BankIntegrationApiTest.kt b/bank/src/test/kotlin/BankIntegrationApiTest.kt
index f84c382c..ba5ff624 100644
--- a/bank/src/test/kotlin/BankIntegrationApiTest.kt
+++ b/bank/src/test/kotlin/BankIntegrationApiTest.kt
@@ -23,10 +23,7 @@ import tech.libeufin.bank.BankAccountCreateWithdrawalResponse
import tech.libeufin.bank.BankWithdrawalOperationPostResponse
import tech.libeufin.bank.BankWithdrawalOperationStatus
import tech.libeufin.bank.WithdrawalStatus
-import tech.libeufin.common.TalerAmount
-import tech.libeufin.common.TalerErrorCode
-import tech.libeufin.common.json
-import tech.libeufin.common.obj
+import tech.libeufin.common.*
import java.util.*
import kotlin.test.assertEquals
@@ -72,7 +69,7 @@ class BankIntegrationApiTest {
// POST /taler-integration/withdrawal-operation/UUID
@Test
fun select() = bankSetup { _ ->
- val reserve_pub = randEddsaPublicKey()
+ val reserve_pub = EddsaPublicKey.rand()
val req = obj {
"reserve_pub" to reserve_pub
"selected_exchange" to exchangePayto.canonical
@@ -110,7 +107,7 @@ class BankIntegrationApiTest {
// Check already selected
client.post("/taler-integration/withdrawal-operation/$uuid") {
json(req) {
- "reserve_pub" to randEddsaPublicKey()
+ "reserve_pub" to EddsaPublicKey.rand()
}
}.assertConflict(TalerErrorCode.BANK_WITHDRAWAL_OPERATION_RESERVE_SELECTION_CONFLICT)
}
@@ -127,14 +124,14 @@ class BankIntegrationApiTest {
// Check unknown account
client.post("/taler-integration/withdrawal-operation/$uuid") {
json {
- "reserve_pub" to randEddsaPublicKey()
+ "reserve_pub" to EddsaPublicKey.rand()
"selected_exchange" to unknownPayto
}
}.assertConflict(TalerErrorCode.BANK_UNKNOWN_ACCOUNT)
// Check account not exchange
client.post("/taler-integration/withdrawal-operation/$uuid") {
json {
- "reserve_pub" to randEddsaPublicKey()
+ "reserve_pub" to EddsaPublicKey.rand()
"selected_exchange" to merchantPayto
}
}.assertConflict(TalerErrorCode.BANK_ACCOUNT_IS_NOT_EXCHANGE)
diff --git a/bank/src/test/kotlin/CoreBankApiTest.kt b/bank/src/test/kotlin/CoreBankApiTest.kt
index 47f4f53f..fbc3492a 100644
--- a/bank/src/test/kotlin/CoreBankApiTest.kt
+++ b/bank/src/test/kotlin/CoreBankApiTest.kt
@@ -942,7 +942,7 @@ class CoreBankTransactionsApiTest {
assertBalance("exchange", "+KUDOS:0")
tx("merchant", "KUDOS:1", "exchange", "") // Bounce common to transaction
tx("merchant", "KUDOS:1", "exchange", "Malformed") // Bounce malformed transaction
- val reservePub = randEddsaPublicKey()
+ val reservePub = EddsaPublicKey.rand()
tx("merchant", "KUDOS:1", "exchange", randIncomingSubject(reservePub)) // Accept incoming
tx("merchant", "KUDOS:1", "exchange", randIncomingSubject(reservePub)) // Bounce reserve_pub reuse
assertBalance("merchant", "-KUDOS:1")
@@ -953,7 +953,7 @@ class CoreBankTransactionsApiTest {
assertBalance("exchange", "+KUDOS:1")
tx("exchange", "KUDOS:1", "merchant", "") // Warn common to transaction
tx("exchange", "KUDOS:1", "merchant", "Malformed") // Warn malformed transaction
- val wtid = randShortHashCode()
+ val wtid = ShortHashCode.rand()
val exchange = ExchangeUrl("http://exchange.example.com/")
tx("exchange", "KUDOS:1", "merchant", randOutgoingSubject(wtid, exchange)) // Accept outgoing
tx("exchange", "KUDOS:1", "merchant", randOutgoingSubject(wtid, exchange)) // Warn wtid reuse
@@ -1129,7 +1129,7 @@ class CoreBankCashoutApiTest {
authRoutine(HttpMethod.Post, "/accounts/merchant/cashouts")
val req = obj {
- "request_uid" to randShortHashCode()
+ "request_uid" to ShortHashCode.rand()
"amount_debit" to "KUDOS:1"
"amount_credit" to convert("KUDOS:1")
}
@@ -1162,7 +1162,7 @@ class CoreBankCashoutApiTest {
// Check insufficient fund
client.postA("/accounts/customer/cashouts") {
json(req) {
- "request_uid" to randShortHashCode()
+ "request_uid" to ShortHashCode.rand()
"amount_debit" to "KUDOS:75"
"amount_credit" to convert("KUDOS:75")
}
@@ -1192,7 +1192,7 @@ class CoreBankCashoutApiTest {
assertBalance("customer", "-KUDOS:1")
client.postA("/accounts/customer/cashouts") {
json(req) {
- "request_uid" to randShortHashCode()
+ "request_uid" to ShortHashCode.rand()
}
}.assertChallenge { _,_->
assertBalance("customer", "-KUDOS:1")
@@ -1216,7 +1216,7 @@ class CoreBankCashoutApiTest {
// Check confirm
client.postA("/accounts/customer/cashouts") {
- json(req) { "request_uid" to randShortHashCode() }
+ json(req) { "request_uid" to ShortHashCode.rand() }
}.assertOkJson<CashoutResponse> {
val id = it.cashout_id
client.getA("/accounts/customer/cashouts/$id")
@@ -1239,7 +1239,7 @@ class CoreBankCashoutApiTest {
// Check get another user's operation
client.postA("/accounts/customer/cashouts") {
- json(req) { "request_uid" to randShortHashCode() }
+ json(req) { "request_uid" to ShortHashCode.rand() }
}.assertOkJson<CashoutResponse> {
val id = it.cashout_id
diff --git a/bank/src/test/kotlin/DatabaseTest.kt b/bank/src/test/kotlin/DatabaseTest.kt
index 3a95d6eb..e41069f7 100644
--- a/bank/src/test/kotlin/DatabaseTest.kt
+++ b/bank/src/test/kotlin/DatabaseTest.kt
@@ -22,7 +22,7 @@ import kotlinx.coroutines.launch
import org.junit.Test
import tech.libeufin.bank.createAdminAccount
import tech.libeufin.bank.db.AccountDAO.AccountCreationResult
-import tech.libeufin.common.oneOrNull
+import tech.libeufin.common.*
import java.time.Duration
import java.time.Instant
import java.time.temporal.ChronoUnit
diff --git a/bank/src/test/kotlin/StatsTest.kt b/bank/src/test/kotlin/StatsTest.kt
index edbd26b6..3f235d4f 100644
--- a/bank/src/test/kotlin/StatsTest.kt
+++ b/bank/src/test/kotlin/StatsTest.kt
@@ -22,10 +22,7 @@ import org.junit.Test
import tech.libeufin.bank.MonitorResponse
import tech.libeufin.bank.MonitorWithConversion
import tech.libeufin.bank.Timeframe
-import tech.libeufin.common.TalerAmount
-import tech.libeufin.common.executeQueryCheck
-import tech.libeufin.common.oneOrNull
-import tech.libeufin.common.toDbMicros
+import tech.libeufin.common.*
import java.time.Instant
import java.time.LocalDateTime
import kotlin.test.assertEquals
@@ -41,7 +38,7 @@ class StatsTest {
db.conn { conn ->
val stmt = conn.prepareStatement("SELECT 0 FROM cashin(?, ?, (?, ?)::taler_amount, ?)")
stmt.setLong(1, Instant.now().toDbMicros()!!)
- stmt.setBytes(2, randShortHashCode().raw)
+ stmt.setBytes(2, ShortHashCode.rand().raw)
val amount = TalerAmount(amount)
stmt.setLong(3, amount.value)
stmt.setInt(4, amount.frac)
diff --git a/bank/src/test/kotlin/WireGatewayApiTest.kt b/bank/src/test/kotlin/WireGatewayApiTest.kt
index 676e455b..82154663 100644
--- a/bank/src/test/kotlin/WireGatewayApiTest.kt
+++ b/bank/src/test/kotlin/WireGatewayApiTest.kt
@@ -21,9 +21,7 @@ import io.ktor.http.*
import org.junit.Test
import tech.libeufin.bank.IncomingHistory
import tech.libeufin.bank.OutgoingHistory
-import tech.libeufin.common.TalerErrorCode
-import tech.libeufin.common.json
-import tech.libeufin.common.obj
+import tech.libeufin.common.*
class WireGatewayApiTest {
// GET /accounts/{USERNAME}/taler-wire-gateway/config
@@ -38,10 +36,10 @@ class WireGatewayApiTest {
@Test
fun transfer() = bankSetup { _ ->
val valid_req = obj {
- "request_uid" to randHashCode()
+ "request_uid" to HashCode.rand()
"amount" to "KUDOS:55"
"exchange_base_url" to "http://exchange.example.com/"
- "wtid" to randShortHashCode()
+ "wtid" to ShortHashCode.rand()
"credit_account" to merchantPayto.canonical
}
@@ -66,7 +64,7 @@ class WireGatewayApiTest {
// Trigger conflict due to reused request_uid
client.postA("/accounts/exchange/taler-wire-gateway/transfer") {
json(valid_req) {
- "wtid" to randShortHashCode()
+ "wtid" to ShortHashCode.rand()
"exchange_base_url" to "http://different-exchange.example.com/"
}
}.assertConflict(TalerErrorCode.BANK_TRANSFER_REQUEST_UID_REUSED)
@@ -81,8 +79,8 @@ class WireGatewayApiTest {
// Unknown account
client.postA("/accounts/exchange/taler-wire-gateway/transfer") {
json(valid_req) {
- "request_uid" to randHashCode()
- "wtid" to randShortHashCode()
+ "request_uid" to HashCode.rand()
+ "wtid" to ShortHashCode.rand()
"credit_account" to unknownPayto
}
}.assertConflict(TalerErrorCode.BANK_UNKNOWN_CREDITOR)
@@ -90,8 +88,8 @@ class WireGatewayApiTest {
// Same account
client.postA("/accounts/exchange/taler-wire-gateway/transfer") {
json(valid_req) {
- "request_uid" to randHashCode()
- "wtid" to randShortHashCode()
+ "request_uid" to HashCode.rand()
+ "wtid" to ShortHashCode.rand()
"credit_account" to exchangePayto
}
}.assertConflict(TalerErrorCode.BANK_ACCOUNT_IS_EXCHANGE)
@@ -106,7 +104,7 @@ class WireGatewayApiTest {
// Bad BASE32 len wtid
client.postA("/accounts/exchange/taler-wire-gateway/transfer") {
json(valid_req) {
- "wtid" to randBase32Crockford(31)
+ "wtid" to randBase32Crockford(31)
}
}.assertBadRequest()
@@ -143,7 +141,7 @@ class WireGatewayApiTest {
},
{
// Transactions using raw bank transaction logic
- tx("merchant", "KUDOS:10", "exchange", "history test with ${randShortHashCode()} reserve pub")
+ tx("merchant", "KUDOS:10", "exchange", "history test with ${ShortHashCode.rand()} reserve pub")
},
{
// Transaction using withdraw logic
@@ -183,7 +181,7 @@ class WireGatewayApiTest {
ignored = listOf(
{
// gnore manual incoming transaction
- tx("exchange", "KUDOS:10", "merchant", "${randShortHashCode()} http://exchange.example.com/")
+ tx("exchange", "KUDOS:10", "merchant", "${ShortHashCode.rand()} http://exchange.example.com/")
},
{
// Ignore malformed incoming transaction
@@ -202,7 +200,7 @@ class WireGatewayApiTest {
fun addIncoming() = bankSetup { _ ->
val valid_req = obj {
"amount" to "KUDOS:44"
- "reserve_pub" to randEddsaPublicKey()
+ "reserve_pub" to EddsaPublicKey.rand()
"debit_account" to merchantPayto.canonical
}
@@ -232,7 +230,7 @@ class WireGatewayApiTest {
// Unknown account
client.postA("/accounts/exchange/taler-wire-gateway/admin/add-incoming") {
json(valid_req) {
- "reserve_pub" to randEddsaPublicKey()
+ "reserve_pub" to EddsaPublicKey.rand()
"debit_account" to unknownPayto
}
}.assertConflict(TalerErrorCode.BANK_UNKNOWN_DEBTOR)
@@ -240,7 +238,7 @@ class WireGatewayApiTest {
// Same account
client.postA("/accounts/exchange/taler-wire-gateway/admin/add-incoming") {
json(valid_req) {
- "reserve_pub" to randEddsaPublicKey()
+ "reserve_pub" to EddsaPublicKey.rand()
"debit_account" to exchangePayto
}
}.assertConflict(TalerErrorCode.BANK_ACCOUNT_IS_EXCHANGE)
diff --git a/bank/src/test/kotlin/helpers.kt b/bank/src/test/kotlin/helpers.kt
index 4fab9d94..e0378d42 100644
--- a/bank/src/test/kotlin/helpers.kt
+++ b/bank/src/test/kotlin/helpers.kt
@@ -206,10 +206,10 @@ suspend fun ApplicationTestBuilder.tx(from: String, amount: String, to: String,
suspend fun ApplicationTestBuilder.transfer(amount: String) {
client.postA("/accounts/exchange/taler-wire-gateway/transfer") {
json {
- "request_uid" to randHashCode()
+ "request_uid" to HashCode.rand()
"amount" to TalerAmount(amount)
"exchange_base_url" to "http://exchange.example.com/"
- "wtid" to randShortHashCode()
+ "wtid" to ShortHashCode.rand()
"credit_account" to merchantPayto
}
}.assertOk()
@@ -221,7 +221,7 @@ suspend fun ApplicationTestBuilder.addIncoming(amount: String) {
pwAuth("admin")
json {
"amount" to TalerAmount(amount)
- "reserve_pub" to randEddsaPublicKey()
+ "reserve_pub" to EddsaPublicKey.rand()
"debit_account" to merchantPayto
}
}.assertOk()
@@ -231,7 +231,7 @@ suspend fun ApplicationTestBuilder.addIncoming(amount: String) {
suspend fun ApplicationTestBuilder.cashout(amount: String) {
val res = client.postA("/accounts/customer/cashouts") {
json {
- "request_uid" to randShortHashCode()
+ "request_uid" to ShortHashCode.rand()
"amount_debit" to amount
"amount_credit" to convert(amount)
}
@@ -241,7 +241,7 @@ suspend fun ApplicationTestBuilder.cashout(amount: String) {
fillCashoutInfo("customer")
client.postA("/accounts/customer/cashouts") {
json {
- "request_uid" to randShortHashCode()
+ "request_uid" to ShortHashCode.rand()
"amount_debit" to amount
"amount_credit" to convert(amount)
}
@@ -290,7 +290,7 @@ suspend fun ApplicationTestBuilder.fillTanInfo(account: String) {
suspend fun ApplicationTestBuilder.withdrawalSelect(uuid: String) {
client.post("/taler-integration/withdrawal-operation/$uuid") {
json {
- "reserve_pub" to randEddsaPublicKey()
+ "reserve_pub" to EddsaPublicKey.rand()
"selected_exchange" to exchangePayto
}
}.assertOk()
@@ -482,18 +482,8 @@ fun HttpRequestBuilder.pwAuth(username: String? = null) {
/* ----- Random data generation ----- */
-fun randBytes(length: Int): ByteArray {
- val bytes = ByteArray(length)
- Random.nextBytes(bytes)
- return bytes
-}
-
fun randBase32Crockford(length: Int) = Base32Crockford.encode(randBytes(length))
-fun randHashCode(): HashCode = HashCode(randBase32Crockford(64))
-fun randShortHashCode(): ShortHashCode = ShortHashCode(randBase32Crockford(32))
-fun randEddsaPublicKey(): EddsaPublicKey = EddsaPublicKey(randBase32Crockford(32))
-
fun randIncomingSubject(reservePub: EddsaPublicKey): String {
return "$reservePub"
}
diff --git a/common/src/main/kotlin/CryptoUtil.kt b/common/src/main/kotlin/CryptoUtil.kt
index 1efdabc4..a4560c29 100644
--- a/common/src/main/kotlin/CryptoUtil.kt
+++ b/common/src/main/kotlin/CryptoUtil.kt
@@ -283,15 +283,6 @@ object CryptoUtil {
return bundle.encoded
}
- fun checkValidEddsaPublicKey(enc: String): Boolean {
- val data = try {
- Base32Crockford.decode(enc)
- } catch (e: Exception) {
- return false
- }
- return data.size == 32
- }
-
fun hashStringSHA256(input: String): ByteArray {
return MessageDigest.getInstance("SHA-256").digest(input.toByteArray(Charsets.UTF_8))
}
diff --git a/common/src/main/kotlin/TalerCommon.kt b/common/src/main/kotlin/TalerCommon.kt
index d4ca47ba..aff68ee3 100644
--- a/common/src/main/kotlin/TalerCommon.kt
+++ b/common/src/main/kotlin/TalerCommon.kt
@@ -299,4 +299,123 @@ class XTalerBankPayto internal constructor(
data class BankPaytoCtx(
val bic: String? = null,
val hostname: String? = null
-) \ No newline at end of file
+)
+
+
+/** 32-byte Crockford's Base32 encoded data */
+@Serializable(with = Base32Crockford32B.Serializer::class)
+class Base32Crockford32B {
+ private var encoded: String? = null
+ val raw: ByteArray
+
+ constructor(encoded: String) {
+ val decoded = try {
+ Base32Crockford.decode(encoded)
+ } catch (e: EncodingException) {
+ null
+ }
+ require(decoded != null && decoded.size == 32) {
+ "expected 32 bytes encoded in Crockford's base32"
+ }
+ this.raw = decoded
+ this.encoded = encoded
+ }
+ constructor(raw: ByteArray) {
+ require(raw.size == 32) {
+ "encoded data should be 32 bytes long"
+ }
+ this.raw = raw
+ }
+
+ fun encoded(): String {
+ encoded = encoded ?: Base32Crockford.encode(raw)
+ return encoded!!
+ }
+
+ override fun toString(): String {
+ return encoded()
+ }
+
+ override fun equals(other: Any?) = (other is Base32Crockford32B) && raw.contentEquals(other.raw)
+
+ internal object Serializer : KSerializer<Base32Crockford32B> {
+ override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Base32Crockford32B", PrimitiveKind.STRING)
+
+ override fun serialize(encoder: Encoder, value: Base32Crockford32B) {
+ encoder.encodeString(value.encoded())
+ }
+
+ override fun deserialize(decoder: Decoder): Base32Crockford32B {
+ return Base32Crockford32B(decoder.decodeString())
+ }
+ }
+
+ companion object {
+ fun rand(): Base32Crockford32B = Base32Crockford32B(randBytes(32))
+ }
+}
+
+/** 64-byte Crockford's Base32 encoded data */
+@Serializable(with = Base32Crockford64B.Serializer::class)
+class Base32Crockford64B {
+ private var encoded: String? = null
+ val raw: ByteArray
+
+ constructor(encoded: String) {
+ val decoded = try {
+ Base32Crockford.decode(encoded)
+ } catch (e: EncodingException) {
+ null
+ }
+
+ require(decoded != null && decoded.size == 64) {
+ "expected 64 bytes encoded in Crockford's base32"
+ }
+ this.raw = decoded
+ this.encoded = encoded
+ }
+ constructor(raw: ByteArray) {
+ require(raw.size == 64) {
+ "encoded data should be 64 bytes long"
+ }
+ this.raw = raw
+ }
+
+ fun encoded(): String {
+ encoded = encoded ?: Base32Crockford.encode(raw)
+ return encoded!!
+ }
+
+ override fun toString(): String {
+ return encoded()
+ }
+
+ override fun equals(other: Any?) = (other is Base32Crockford64B) && raw.contentEquals(other.raw)
+
+ internal object Serializer : KSerializer<Base32Crockford64B> {
+ override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Base32Crockford64B", PrimitiveKind.STRING)
+
+ override fun serialize(encoder: Encoder, value: Base32Crockford64B) {
+ encoder.encodeString(value.encoded())
+ }
+
+ override fun deserialize(decoder: Decoder): Base32Crockford64B {
+ return Base32Crockford64B(decoder.decodeString())
+ }
+ }
+
+ companion object {
+ fun rand(): Base32Crockford64B = Base32Crockford64B(randBytes(64))
+ }
+}
+
+/** 32-byte hash code */
+typealias ShortHashCode = Base32Crockford32B
+/** 64-byte hash code */
+typealias HashCode = Base32Crockford64B
+/**
+ * EdDSA and ECDHE public keys always point on Curve25519
+ * and represented using the standard 256 bits Ed25519 compact format,
+ * converted to Crockford Base32.
+ */
+typealias EddsaPublicKey = Base32Crockford32B \ No newline at end of file
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Metadata.kt b/common/src/main/kotlin/TxMedatada.kt
index 8bfbe80c..f6741447 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/Metadata.kt
+++ b/common/src/main/kotlin/TxMedatada.kt
@@ -1,6 +1,6 @@
/*
* This file is part of LibEuFin.
- * Copyright (C) 2023 Taler Systems S.A.
+ * Copyright (C) 2024 Taler Systems S.A.
* LibEuFin is free software; you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
@@ -16,15 +16,12 @@
* License along with LibEuFin; see the file COPYING. If not, see
* <http://www.gnu.org/licenses/>
*/
-package tech.libeufin.bank
+package tech.libeufin.common
private val PATTERN = Regex("[a-z0-9A-Z]{52}")
-fun parseIncomingTxMetadata(subject: String): EddsaPublicKey? {
- val match = PATTERN.find(subject)?.value ?: return null
- try {
- return EddsaPublicKey(match)
- } catch (e: Exception) {
- return null
- }
+/** Extract the reserve public key from an incoming Taler transaction subject */
+fun parseIncomingTxMetadata(subject: String): EddsaPublicKey {
+ val match = PATTERN.find(subject)?.value ?: throw Exception("Missing reserve public key")
+ return EddsaPublicKey(match)
} \ No newline at end of file
diff --git a/common/src/main/kotlin/random.kt b/common/src/main/kotlin/random.kt
new file mode 100644
index 00000000..d939ab80
--- /dev/null
+++ b/common/src/main/kotlin/random.kt
@@ -0,0 +1,28 @@
+/*
+ * This file is part of LibEuFin.
+ * Copyright (C) 2024 Taler Systems S.A.
+ *
+ * LibEuFin is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation; either version 3, or
+ * (at your option) any later version.
+ *
+ * LibEuFin is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General
+ * Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with LibEuFin; see the file COPYING. If not, see
+ * <http://www.gnu.org/licenses/>
+ */
+
+package tech.libeufin.common
+
+import kotlin.random.Random
+
+fun randBytes(length: Int): ByteArray {
+ val bytes = ByteArray(length)
+ Random.nextBytes(bytes)
+ return bytes
+} \ No newline at end of file
diff --git a/common/src/test/kotlin/CryptoUtilTest.kt b/common/src/test/kotlin/CryptoUtilTest.kt
index b14ada07..a4778369 100644
--- a/common/src/test/kotlin/CryptoUtilTest.kt
+++ b/common/src/test/kotlin/CryptoUtilTest.kt
@@ -142,14 +142,6 @@ class CryptoUtilTest {
}
@Test
- fun checkEddsaPublicKey() {
- val givenEnc = "XZH3P6NF9DSG3BH0C082X38N2RVK1RV2H24KF76028QBKDM24BCG"
- val non32bytes = "N2RVK1RV2H24KF76028QBKDM24BCG"
- assertTrue(CryptoUtil.checkValidEddsaPublicKey(givenEnc))
- assertFalse(CryptoUtil.checkValidEddsaPublicKey(non32bytes))
- }
-
- @Test
fun base32Test() {
val validKey = "4MZT6RS3RVB3B0E2RDMYW0YRA3Y0VPHYV0CYDE6XBB0YMPFXCEG0"
val enc = validKey
diff --git a/contrib/ci/jobs/0-codespell/job.sh b/contrib/ci/jobs/0-codespell/job.sh
index 52a5693e..010d9ada 100755
--- a/contrib/ci/jobs/0-codespell/job.sh
+++ b/contrib/ci/jobs/0-codespell/job.sh
@@ -20,6 +20,7 @@ configure~
*/*.xsd
*/*.xml
*/ebics/src/test/kotlin/EbicsOrderUtilTest.kt
+*/common/src/main/kotlin/TalerErrorCode.kt
EOF
);
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Database.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Database.kt
index b4179f51..956ae58c 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/Database.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/Database.kt
@@ -273,7 +273,7 @@ class Database(dbConfig: String): DbPool(dbConfig, "libeufin_nexus") {
*/
suspend fun registerTalerableIncoming(
paymentData: IncomingPayment,
- reservePub: ByteArray
+ reservePub: EddsaPublicKey
): IncomingRegistrationResult = conn { conn ->
val stmt = conn.prepareStatement("""
SELECT out_found, out_tx_id
@@ -294,7 +294,7 @@ class Database(dbConfig: String): DbPool(dbConfig, "libeufin_nexus") {
stmt.setLong(4, executionTime)
stmt.setString(5, paymentData.debitPaytoUri)
stmt.setString(6, paymentData.bankId)
- stmt.setBytes(7, reservePub)
+ stmt.setBytes(7, reservePub.raw)
stmt.executeQuery().use {
when {
!it.next() -> throw Exception("Inserting talerable incoming payment gave no outcome")
@@ -348,13 +348,13 @@ class Database(dbConfig: String): DbPool(dbConfig, "libeufin_nexus") {
* @param maybeReservePub reserve public key to look up
* @return true if found, false otherwise
*/
- suspend fun isReservePubFound(maybeReservePub: ByteArray): Boolean = conn { conn ->
+ suspend fun isReservePubFound(maybeReservePub: EddsaPublicKey): Boolean = conn { conn ->
val stmt = conn.prepareStatement("""
SELECT 1
FROM talerable_incoming_transactions
WHERE reserve_public_key = ?;
""")
- stmt.setBytes(1, maybeReservePub)
+ stmt.setBytes(1, maybeReservePub.raw)
val res = stmt.executeQuery()
res.use {
return@conn it.next()
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsFetch.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsFetch.kt
index 2b1a12a9..cd69ada1 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsFetch.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsFetch.kt
@@ -130,64 +130,6 @@ private fun makeTalerFrac(bankFrac: String): Int {
}
/**
- * Converts valid reserve pubs to its binary representation.
- *
- * @param maybeReservePub input.
- * @return [ByteArray] or null if not valid.
- */
-fun isReservePub(maybeReservePub: String): ByteArray? {
- if (maybeReservePub.length != 52) {
- logger.error("Not a reserve pub, length (${maybeReservePub.length}) is not 52")
- return null
- }
- val dec = try {
- Base32Crockford.decode(maybeReservePub)
- } catch (e: EncodingException) {
- logger.error("Not a reserve pub: $maybeReservePub")
- return null
- }
- logger.debug("Reserve how many bytes: ${dec.size}")
- // this check would only be effective after #7980
- if (dec.size != 32) {
- logger.error("Not a reserve pub, wrong length: ${dec.size}")
- return null
- }
- return dec
-}
-
-/**
- * Extract the part of the subject that might represent a
- * valid Taler reserve public key. That addresses some bank
- * policies of adding extra information around the payment
- * subject.
- *
- * @param subject raw subject as read from the bank.
- */
-fun removeSubjectNoise(subject: String): String? {
- val re = "\\b[a-z0-9A-Z]{52}\\b".toRegex()
- val result = re.find(subject.replace("[\n]+".toRegex(), "")) ?: return null
- return result.value
-}
-
-/**
- * Checks the two conditions that may invalidate one incoming
- * payment: subject validity and availability.
- *
- * @param payment incoming payment whose subject is to be checked.
- * @return [ByteArray] as the reserve public key, or null if the
- * payment cannot lead to a Taler withdrawal.
- */
-private suspend fun getTalerReservePub(
- payment: IncomingPayment
-): ByteArray? {
- // Removing noise around the potential reserve public key.
- val maybeReservePub = removeSubjectNoise(payment.wireTransferSubject) ?: return null
- // Checking validity first.
- val dec = isReservePub(maybeReservePub) ?: return null
- return dec
-}
-
-/**
* Ingests an outgoing payment. It links it to the initiated
* outgoing transaction that originated it.
*
@@ -209,6 +151,8 @@ suspend fun ingestOutgoingPayment(
}
}
+private val PATTERN = Regex("[a-z0-9A-Z]{52}")
+
/**
* Ingests an incoming payment. Stores the payment into valid talerable ones
* or bounces it, according to the subject.
@@ -221,26 +165,28 @@ suspend fun ingestIncomingPayment(
db: Database,
payment: IncomingPayment
) {
- val reservePub = getTalerReservePub(payment)
- if (reservePub == null) {
- val result = db.registerMalformedIncoming(
- payment,
- payment.amount,
- Instant.now()
- )
- if (result.new) {
- logger.info("$payment bounced in '${result.bounceId}'")
- } else {
- logger.debug("IN '${payment.bankId}' already seen and bounced in '${result.bounceId}'")
- }
- } else {
- val result = db.registerTalerableIncoming(payment, reservePub)
- if (result.new) {
- logger.info("$payment")
- } else {
- logger.debug("IN '${payment.bankId}' already seen")
+ runCatching { parseIncomingTxMetadata(payment.wireTransferSubject) }.fold(
+ onSuccess = { reservePub ->
+ val result = db.registerTalerableIncoming(payment, reservePub)
+ if (result.new) {
+ logger.info("$payment")
+ } else {
+ logger.debug("IN '${payment.bankId}' already seen")
+ }
+ },
+ onFailure = { e ->
+ val result = db.registerMalformedIncoming(
+ payment,
+ payment.amount,
+ Instant.now()
+ )
+ if (result.new) {
+ logger.info("$payment bounced in '${result.bounceId}': ${e.message}")
+ } else {
+ logger.debug("IN '${payment.bankId}' already seen and bounced in '${result.bounceId}': ${e.message}")
+ }
}
- }
+ )
}
private fun ingestDocument(
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022.kt
index a98c7cd6..6d9a259a 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022.kt
@@ -275,7 +275,7 @@ fun parseTxNotif(
"CRDT" -> {
val bankId: String = one("Refs").one("AcctSvcrRef").text()
// Obtaining payment subject.
- val subject = opt("RmtInf")?.map("Ustrd") { text() }?.joinToString()
+ val subject = opt("RmtInf")?.map("Ustrd") { text() }?.joinToString("")
if (subject == null) {
logger.debug("Skip notification '$bankId', missing subject")
return@notificationForEachTx
diff --git a/nexus/src/test/kotlin/DatabaseTest.kt b/nexus/src/test/kotlin/DatabaseTest.kt
index 050bbb25..1f27a782 100644
--- a/nexus/src/test/kotlin/DatabaseTest.kt
+++ b/nexus/src/test/kotlin/DatabaseTest.kt
@@ -18,7 +18,7 @@
*/
import org.junit.Test
-import tech.libeufin.common.TalerAmount
+import tech.libeufin.common.*
import tech.libeufin.nexus.DatabaseSubmissionState
import tech.libeufin.nexus.InitiatedPayment
import tech.libeufin.nexus.PaymentInitiationOutcome
@@ -116,8 +116,7 @@ class IncomingPaymentsTest {
// Tests the creation of a talerable incoming payment.
@Test
fun talerable() = setup { db, _ ->
- val reservePub = ByteArray(32)
- Random.nextBytes(reservePub)
+ val reservePub = EddsaPublicKey.rand()
val inc = genInPay("reserve-pub")
// Checking the reserve is not found.
diff --git a/nexus/src/test/kotlin/Parsing.kt b/nexus/src/test/kotlin/Parsing.kt
index 08fb02fd..f5cbfb9c 100644
--- a/nexus/src/test/kotlin/Parsing.kt
+++ b/nexus/src/test/kotlin/Parsing.kt
@@ -18,69 +18,63 @@
*/
import org.junit.Test
-import tech.libeufin.common.TalerAmount
+import tech.libeufin.common.*
import tech.libeufin.nexus.getAmountNoCurrency
-import tech.libeufin.nexus.isReservePub
-import tech.libeufin.nexus.removeSubjectNoise
-import kotlin.test.assertEquals
-import kotlin.test.assertFailsWith
-import kotlin.test.assertNotNull
-import kotlin.test.assertNull
+import kotlin.test.*
class Parsing {
@Test
fun reservePublicKey() {
- assertNull(removeSubjectNoise("does not contain any reserve"))
- // 4MZT6RS3RVB3B0E2RDMYW0YRA3Y0VPHYV0CYDE6XBB0YMPFXCEG0
- assertNotNull(removeSubjectNoise("4MZT6RS3RVB3B0E2RDMYW0YRA3Y0VPHYV0CYDE6XBB0YMPFXCEG0"))
+ assertFails { parseIncomingTxMetadata("does not contain any reserve") }
+
assertEquals(
- "4MZT6RS3RVB3B0E2RDMYW0YRA3Y0VPHYV0CYDE6XBB0YMPFXCEG0",
- removeSubjectNoise(
+ EddsaPublicKey("4MZT6RS3RVB3B0E2RDMYW0YRA3Y0VPHYV0CYDE6XBB0YMPFXCEG0"),
+ parseIncomingTxMetadata(
"noise 4MZT6RS3RVB3B0E2RDMYW0YRA3Y0VPHYV0CYDE6XBB0YMPFXCEG0 noise"
)
)
assertEquals(
- "4MZT6RS3RVB3B0E2RDMYW0YRA3Y0VPHYV0CYDE6XBB0YMPFXCEG0",
- removeSubjectNoise(
+ EddsaPublicKey("4MZT6RS3RVB3B0E2RDMYW0YRA3Y0VPHYV0CYDE6XBB0YMPFXCEG0"),
+ parseIncomingTxMetadata(
"4MZT6RS3RVB3B0E2RDMYW0YRA3Y0VPHYV0CYDE6XBB0YMPFXCEG0 noise to the right"
)
)
assertEquals(
- "4MZT6RS3RVB3B0E2RDMYW0YRA3Y0VPHYV0CYDE6XBB0YMPFXCEG0",
- removeSubjectNoise(
+ EddsaPublicKey("4MZT6RS3RVB3B0E2RDMYW0YRA3Y0VPHYV0CYDE6XBB0YMPFXCEG0"),
+ parseIncomingTxMetadata(
"noise to the left 4MZT6RS3RVB3B0E2RDMYW0YRA3Y0VPHYV0CYDE6XBB0YMPFXCEG0"
)
)
assertEquals(
- "4MZT6RS3RVB3B0E2RDMYW0YRA3Y0VPHYV0CYDE6XBB0YMPFXCEG0",
- removeSubjectNoise(
+ EddsaPublicKey("4MZT6RS3RVB3B0E2RDMYW0YRA3Y0VPHYV0CYDE6XBB0YMPFXCEG0"),
+ parseIncomingTxMetadata(
" 4MZT6RS3RVB3B0E2RDMYW0YRA3Y0VPHYV0CYDE6XBB0YMPFXCEG0 "
)
)
assertEquals(
- "4MZT6RS3RVB3B0E2RDMYW0YRA3Y0VPHYV0CYDE6XBB0YMPFXCEG0",
- removeSubjectNoise("""
+ EddsaPublicKey("4MZT6RS3RVB3B0E2RDMYW0YRA3Y0VPHYV0CYDE6XBB0YMPFXCEG0"),
+ parseIncomingTxMetadata("""
noise
4MZT6RS3RVB3B0E2RDMYW0YRA3Y0VPHYV0CYDE6XBB0YMPFXCEG0
noise
""")
)
// Got the first char removed.
- assertNull(removeSubjectNoise("MZT6RS3RVB3B0E2RDMYW0YRA3Y0VPHYV0CYDE6XBB0YMPFXCEG0"))
+ assertFails { parseIncomingTxMetadata("MZT6RS3RVB3B0E2RDMYW0YRA3Y0VPHYV0CYDE6XBB0YMPFXCEG0") }
}
@Test // Could be moved in a dedicated Amounts.kt test module.
fun generateCurrencyAgnosticAmount() {
- assertFailsWith<Exception> {
+ assertFails {
// Too many fractional digits.
getAmountNoCurrency(TalerAmount(1, 123456789, "KUDOS"))
}
- assertFailsWith<Exception> {
+ assertFails {
// Nexus doesn't support sub-cents.
getAmountNoCurrency(TalerAmount(1, 12345678, "KUDOS"))
}
- assertFailsWith<Exception> {
+ assertFails {
// Nexus doesn't support sub-cents.
getAmountNoCurrency(TalerAmount(0, 1, "KUDOS"))
}
@@ -93,20 +87,4 @@ class Parsing {
getAmountNoCurrency(TalerAmount(0, 10000000, "KUDOS"))
)
}
-
- // Checks that the input decodes to a 32-bytes value.
- @Test
- fun validateReservePub() {
- val valid = "4MZT6RS3RVB3B0E2RDMYW0YRA3Y0VPHYV0CYDE6XBB0YMPFXCEG0"
- val validBytes = isReservePub(valid)
- assertNotNull(validBytes)
- assertEquals(32, validBytes.size)
- assertNull(isReservePub("noise"))
- val trimmedInput = valid.dropLast(10)
- assertNull(isReservePub(trimmedInput))
- val invalidChar = StringBuilder(valid)
- invalidChar.setCharAt(10, '*')
- assertNull(isReservePub(invalidChar.toString()))
- assertNull(isReservePub(valid.dropLast(1)))
- }
} \ No newline at end of file
diff --git a/testbench/src/main/kotlin/Main.kt b/testbench/src/main/kotlin/Main.kt
index 8586c2aa..cf50c59d 100644
--- a/testbench/src/main/kotlin/Main.kt
+++ b/testbench/src/main/kotlin/Main.kt
@@ -34,12 +34,6 @@ import java.time.Instant
import kotlinx.coroutines.runBlocking
import io.ktor.client.request.*
-fun randBytes(length: Int): ByteArray {
- val bytes = ByteArray(length)
- kotlin.random.Random.nextBytes(bytes)
- return bytes
-}
-
val nexusCmd = LibeufinNexusCommand()
val client = HttpClient(CIO)
diff --git a/testbench/src/test/kotlin/IntegrationTest.kt b/testbench/src/test/kotlin/IntegrationTest.kt
index ddc43ced..b398ad03 100644
--- a/testbench/src/test/kotlin/IntegrationTest.kt
+++ b/testbench/src/test/kotlin/IntegrationTest.kt
@@ -50,12 +50,6 @@ fun HttpResponse.assertNoContent() {
assertEquals(HttpStatusCode.NoContent, this.status)
}
-fun randBytes(length: Int): ByteArray {
- val bytes = ByteArray(length)
- kotlin.random.Random.nextBytes(bytes)
- return bytes
-}
-
fun server(lambda: () -> Unit) {
// Start the HTTP server in another thread
kotlin.concurrent.thread(isDaemon = true) {
@@ -148,11 +142,11 @@ class IntegrationTest {
it.execSQLUpdate("SET search_path TO libeufin_nexus;")
}
- val reservePub = randBytes(32)
+ val reservePub = EddsaPublicKey.rand()
val payment = IncomingPayment(
amount = TalerAmount("EUR:10"),
debitPaytoUri = userPayTo.toString(),
- wireTransferSubject = "Error test ${Base32Crockford.encode(reservePub)}",
+ wireTransferSubject = "Error test $reservePub",
executionTime = Instant.now(),
bankId = "error"
)
@@ -211,7 +205,7 @@ class IntegrationTest {
ingestIncomingPayment(db, IncomingPayment(
amount = TalerAmount("EUR:10"),
debitPaytoUri = userPayTo.toString(),
- wireTransferSubject = "Success ${Base32Crockford.encode(randBytes(32))}",
+ wireTransferSubject = "Success ${Base32Crockford32B.rand().encoded()}",
executionTime = Instant.now(),
bankId = "success"
))
@@ -278,9 +272,9 @@ class IntegrationTest {
// Cashin
repeat(3) { i ->
- val reservePub = randBytes(32)
+ val reservePub = EddsaPublicKey.rand()
val amount = TalerAmount("EUR:${20+i}")
- val subject = "cashin test $i: ${Base32Crockford.encode(reservePub)}"
+ val subject = "cashin test $i: $reservePub"
nexusCmd.run("testing fake-incoming $flags --subject \"$subject\" --amount $amount $userPayTo")
val converted = client.get("http://0.0.0.0:8080/conversion-info/cashin-rate?amount_debit=EUR:${20 + i}")
.assertOkJson<ConversionResponse>().amount_credit
@@ -296,20 +290,20 @@ class IntegrationTest {
}.assertOkJson<IncomingHistory> {
val tx = it.incoming_transactions.first()
assertEquals(converted, tx.amount)
- assert(reservePub.contentEquals(tx.reserve_pub.raw))
+ assertEquals(reservePub, tx.reserve_pub)
}
}
// Cashout
repeat(3) { i ->
- val requestUid = randBytes(32)
+ val requestUid = ShortHashCode.rand()
val amount = TalerAmount("KUDOS:${10+i}")
val convert = client.get("http://0.0.0.0:8080/conversion-info/cashout-rate?amount_debit=$amount")
.assertOkJson<ConversionResponse>().amount_credit
client.post("http://0.0.0.0:8080/accounts/customer/cashouts") {
basicAuth("customer", "password")
json {
- "request_uid" to ShortHashCode(requestUid)
+ "request_uid" to requestUid
"amount_debit" to amount
"amount_credit" to convert
}