diff options
author | Antoine A <> | 2024-02-25 19:06:50 +0100 |
---|---|---|
committer | Antoine A <> | 2024-02-25 19:06:50 +0100 |
commit | 0067499c54fc60784f2cf2ec9be8340da7a5459e (patch) | |
tree | b4a6029887d25859e7ebaa1b1960986981c8458f | |
parent | b421a7b263d97ade06c7e87dda543d4b7706f59d (diff) | |
download | libeufin-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
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 } |