commit 2cce8ae13da4e1592bf83ffca37afc64f26fcf8f
parent 7153ffba0a060cb6131421933fd8f978d4c9487a
Author: Antoine A <>
Date: Wed, 26 Jun 2024 23:19:09 +0200
common: implemente a more lenient wire transfer subject parser
Diffstat:
3 files changed, 148 insertions(+), 72 deletions(-)
diff --git a/common/src/main/kotlin/TxMedatada.kt b/common/src/main/kotlin/TxMedatada.kt
@@ -18,12 +18,56 @@
*/
package tech.libeufin.common
+private val BASE32_32B_UPPER_PATTERN = Regex("[0-9A-Z]{52}")
private val BASE32_32B_PATTERN = Regex("[a-z0-9A-Z]{52}")
+private val CLEAN_PATTERN = Regex(" ?[\\n\\-\\+] ?")
-/** Extract the reserve public key from an incoming Taler transaction subject */
+/**
+ * Extract the reserve public key from an incoming Taler transaction subject
+ *
+ * We first try to match an uppercase key then a lowercase key. If none are
+ * found we clean the subject and retry.
+ **/
fun parseIncomingTxMetadata(subject: String): EddsaPublicKey {
- val match = BASE32_32B_PATTERN.find(subject)?.value ?: throw Exception("Missing reserve public key")
- return EddsaPublicKey(match)
+ /**
+ * Extract the reserve public key from [subject] using [pattern]
+ *
+ * Return null if found none and throw if find too many
+ **/
+ fun run(subject: String, pattern: Regex): EddsaPublicKey? {
+ val matches = pattern.findAll(subject).iterator()
+ if (!matches.hasNext()) return null
+ val match = matches.next()
+ if (matches.hasNext()) {
+ //val count = matches.count()
+ throw Exception("Found multiple reserve public key")
+ }
+ return EddsaPublicKey(match.value)
+ }
+
+ // Wire transfer subjects are generally small in size, and not
+ // being able to find the encoded reserve public key poses a huge
+ // usability problem. So we're ready to work hard to find it.
+
+ // Bank interfaces doesn't always allow a complete encoded key to
+ // be entered on a single line, so users have to split them, which
+ // may lead them to add an erroneous space or + or - separator.
+ // If we can't find a key, we try to eliminate these common errors
+ // before trying again.
+
+ // Since any sequence of 52 upper and lowercase characters can be a
+ // valid encoded key, deleting spaces and separators can create false
+ // positives, so we always start by searching for a key coded in
+ // uppercase and then mixed once.
+
+ val matchWithoutCleaning = run(subject, BASE32_32B_UPPER_PATTERN)
+ ?: run(subject, BASE32_32B_PATTERN)
+ if (matchWithoutCleaning != null) return matchWithoutCleaning
+
+ val cleaned = subject.replace(CLEAN_PATTERN, "")
+ return run(cleaned, BASE32_32B_UPPER_PATTERN)
+ ?: run(cleaned, BASE32_32B_PATTERN)
+ ?: throw Exception("Missing reserve public key")
}
/** Extract the reserve public key from an incoming Taler transaction subject */
diff --git a/common/src/test/kotlin/TxMedataTest.kt b/common/src/test/kotlin/TxMedataTest.kt
@@ -0,0 +1,100 @@
+/*
+ * 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/>
+ */
+
+import tech.libeufin.common.*
+import kotlin.test.*
+
+class TxMetadataTest{
+ fun assertFailsMsg(msg: String, lambda: () -> Unit) {
+ val failure = assertFails(lambda)
+ assertEquals(msg, failure.message)
+ }
+
+ @Test
+ fun parse() {
+ val upper = "4MZT6RS3RVB3B0E2RDMYW0YRA3Y0VPHYV0CYDE6XBB0YMPFXCEG0"
+ val upperL = "4MZT6RS3RVB3B0E2RDMYW0YRA3"
+ val upperR = "Y0VPHYV0CYDE6XBB0YMPFXCEG0"
+ val mixed = "4mzt6RS3rvb3b0e2rdmyw0yra3y0vphyv0cyde6xbb0ympfxceg0"
+ val mixedL = "4mzt6rs3rvb3b0e2rdmyw0yra3"
+ val mixedR = "y0vphyv0cyde6xbb0ympfxceg0"
+ val otherUpper = "TEST6RS3RVB3B0E2RDMYW0YRA3Y0VPHYV0CYDE6XBB0YMPFXCEG0"
+ val otherMixed = "test6rRSrvb3b0e2rdmyw0yra3y0vphyv0cyde6xbb0ympfxceg0"
+ val key = EddsaPublicKey(upper)
+
+ // Check succeed if upper or mixed
+ for (case in sequenceOf(upper, mixed)) {
+ for (test in sequenceOf(
+ "noise $case noise",
+ "$case noise to the right",
+ "noise to the left $case",
+ " $case ",
+ "noise\n$case\nnoise",
+ "Test+$case"
+ )) {
+ assertEquals(key, parseIncomingTxMetadata(test))
+ }
+ }
+
+ // Check succeed if upper or mixed and split
+ for ((L, R) in sequenceOf(upperL to upperR, mixedL to mixedR)) {
+ for (case in sequenceOf(
+ "left $L$R right",
+ "left $L-$R right",
+ "left $L+$R right",
+ "left $L\n$R right",
+ "left $L-+\n$R right",
+ "left $L - $R right",
+ "left $L + $R right",
+ "left $L \n $R right",
+ "left $L - + \n $R right",
+ )) {
+ assertEquals(key, parseIncomingTxMetadata(case))
+ }
+ }
+
+ // Check prefer upper
+ for (case in sequenceOf(
+ "$mixed $upper",
+ "$mixedL-$mixedR $upperL-$upperR"
+ )) {
+ assertEquals(key, parseIncomingTxMetadata(case))
+ }
+
+ // Check failure when multiple keys match
+ for (case in sequenceOf(
+ "$upper $upper", // Both upper
+ "$mixed $mixed" // Both mixed
+ )) {
+ assertFailsMsg("Found multiple reserve public key") {
+ parseIncomingTxMetadata(case)
+ }
+ }
+
+ // Check failure if malformed or missing
+ for (case in sequenceOf(
+ "does not contain any reserve", // Check fail if none
+ upper.substring(0, upper.length-1) // Check fail if missing char
+ )) {
+ assertFailsMsg("Missing reserve public key") {
+ parseIncomingTxMetadata(case)
+ }
+ }
+ }
+}
+\ No newline at end of file
diff --git a/nexus/src/test/kotlin/Parsing.kt b/nexus/src/test/kotlin/Parsing.kt
@@ -1,68 +0,0 @@
-/*
- * 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/>
- */
-
-import org.junit.Test
-import tech.libeufin.common.EddsaPublicKey
-import tech.libeufin.common.TalerAmount
-import tech.libeufin.common.parseIncomingTxMetadata
-import tech.libeufin.nexus.getAmountNoCurrency
-import kotlin.test.assertEquals
-import kotlin.test.assertFails
-
-class Parsing {
-
- @Test
- fun reservePublicKey() {
- assertFails { parseIncomingTxMetadata("does not contain any reserve") }
- val encoded = "4MZT6RS3RVB3B0E2RDMYW0YRA3Y0VPHYV0CYDE6XBB0YMPFXCEG0"
- val key = EddsaPublicKey(encoded)
- assertEquals(key, parseIncomingTxMetadata("noise $encoded noise"))
- assertEquals(key, parseIncomingTxMetadata("$encoded noise to the right"))
- assertEquals(key, parseIncomingTxMetadata("noise to the left $encoded"))
- assertEquals(key, parseIncomingTxMetadata(" $encoded "))
- assertEquals(key, parseIncomingTxMetadata("noise\n$encoded\nnoise"))
- assertEquals(key, parseIncomingTxMetadata("Test+$encoded"))
- // Got the first char removed.
- assertFails { parseIncomingTxMetadata("MZT6RS3RVB3B0E2RDMYW0YRA3Y0VPHYV0CYDE6XBB0YMPFXCEG0") }
- }
-
- @Test // Could be moved in a dedicated Amounts.kt test module.
- fun generateCurrencyAgnosticAmount() {
- assertFails {
- // Too many fractional digits.
- getAmountNoCurrency(TalerAmount(1, 123456789, "KUDOS"))
- }
- assertFails {
- // Nexus doesn't support sub-cents.
- getAmountNoCurrency(TalerAmount(1, 12345678, "KUDOS"))
- }
- assertFails {
- // Nexus doesn't support sub-cents.
- getAmountNoCurrency(TalerAmount(0, 1, "KUDOS"))
- }
- assertEquals(
- "0.01",
- getAmountNoCurrency(TalerAmount(0, 1000000, "KUDOS"))
- )
- assertEquals(
- "0.1",
- getAmountNoCurrency(TalerAmount(0, 10000000, "KUDOS"))
- )
- }
-}
-\ No newline at end of file