libeufin

Integration and sandbox testing for FinTech APIs and data formats
Log | Files | Refs | Submodules | README | LICENSE

commit 411fbc4e1e12f3a6528feb54b57db72d540b4363
parent af013f998ce68546e1f0a5ec17b29484283dad0e
Author: Antoine A <>
Date:   Sat, 11 Jan 2025 01:30:45 +0100

common: improve KYC subject parsing

Diffstat:
Mcommon/src/main/kotlin/Subject.kt | 63+++++++++++++++++++++++++++++++++------------------------------
Mcommon/src/test/kotlin/SubjectTest.kt | 18++++++++++++++----
2 files changed, 47 insertions(+), 34 deletions(-)

diff --git a/common/src/main/kotlin/Subject.kt b/common/src/main/kotlin/Subject.kt @@ -57,6 +57,9 @@ private enum class Base32Quality { private data class Candidate(val subject: IncomingSubject, val quality: Base32Quality) +private const val KEY_SIZE = 52; +private const val KYC_SIZE = KEY_SIZE + 3; + /** * Extract the public key from an unstructured incoming transfer subject. * @@ -70,10 +73,14 @@ fun parseIncomingSubject(subject: String): IncomingSubject { /** Parse an incoming subject */ fun parseSingle(str: String): Candidate? { // Check key type - val (isKyc, raw) = if (str.startsWith("KYC:")) { - Pair(true, str.substring(4)) - } else { - Pair(false, str) + val (isKyc, raw) = when (str.length) { + KEY_SIZE -> Pair(false, str) + KYC_SIZE -> if (str.startsWith("KYC")) { + Pair(true, str.substring(3)) + } else { + return null + } + else -> return null } // Check key validity @@ -96,7 +103,7 @@ fun parseIncomingSubject(subject: String): IncomingSubject { val concatenated = StringBuilder() var part: Int? = null for ((i, c) in subject.withIndex()) { - if (c.isLetterOrDigit() || c == ':') { + if (c.isLetterOrDigit()) { part = part ?: i } else if (part != null) { val start = concatenated.length @@ -120,36 +127,32 @@ fun parseIncomingSubject(subject: String): IncomingSubject { // Use progressively longer concatenation for (end in parts.subList(i, parts.size)) { val range = start.start..end.endInclusive - val len = range.count() // Until they are to long to be a key - if (len > 56) { + if (range.count() > KYC_SIZE) { break; } - // If the slice is the right size for a key (56B with prefix else 54B) - if (len == 52 || len == 56) { - // Parse the concatenated parts - val slice = concatenated.substring(range) - parseSingle(slice)?.let { other -> - if (best != null) { - if (other.quality > best.quality // We prefer high quality keys - || ( // We prefer prefixed keys over reserve keys - best.subject.type == IncomingType.reserve && - (other.subject.type == IncomingType.kyc || other.subject.type == IncomingType.wad) - )) - { - best = other - } else if (best.subject.key != other.subject.key // If keys are different - && best.quality == other.quality // Of same quality - && !( // And prefixing is different - (best.subject.type == IncomingType.kyc || best.subject.type == IncomingType.wad) && - other.subject.type == IncomingType.reserve - )) - { - throw Exception("Found multiple reserve public key") - } - } else { + // Parse the concatenated parts + val slice = concatenated.substring(range) + parseSingle(slice)?.let { other -> + if (best != null) { + if (other.quality > best.quality // We prefer high quality keys + || ( // We prefer prefixed keys over reserve keys + best.subject.type == IncomingType.reserve && + (other.subject.type == IncomingType.kyc || other.subject.type == IncomingType.wad) + )) + { best = other + } else if (best.subject.key != other.subject.key // If keys are different + && best.quality == other.quality // Of same quality + && !( // And prefixing is different + (best.subject.type == IncomingType.kyc || best.subject.type == IncomingType.wad) && + other.subject.type == IncomingType.reserve + )) + { + throw Exception("Found multiple reserve public key") } + } else { + best = other } } } diff --git a/common/src/test/kotlin/SubjectTest.kt b/common/src/test/kotlin/SubjectTest.kt @@ -32,11 +32,11 @@ class SubjectTest { val other = "00Q979QSMJ29S7BJT3DDAVC5A0DR5Z05B7N0QT1RCBQ8FXJPZ6RG"; for (ty in sequenceOf(IncomingType.reserve, IncomingType.kyc)) { - val prefix = if (ty == IncomingType.kyc) "KYC:" else ""; + val prefix = if (ty == IncomingType.kyc) "KYC" else ""; val standard = "$prefix$key" - val (standardL, standardR) = standard.chunked(standard.length / 2) + val (standardL, standardR) = standard.chunked(standard.length / 2 + 1) val mixed = "${prefix}4mzt6RS3rvb3b0e2rdmyw0yra3y0vphyv0cyde6xbb0ympfxceg0" - val (mixedL, mixedR) = mixed.chunked(mixed.length / 2) + val (mixedL, mixedR) = mixed.chunked(mixed.length / 2 + 1) val other_standard = "$prefix$other" val other_mixed = "${prefix}TEGY6d9mh9pgwvwpgs0z0095z854xegfy7jj202yd0esp8p0za60" val key = IncomingSubject(ty, EddsaPublicKey(key)) @@ -125,16 +125,26 @@ class SubjectTest { /** Test parsing logic using real use case */ @Test fun real() { - // Good cases + // Good reserve cases for ((subject, key) in sequenceOf( "Taler TEGY6d9mh9pgwvwpgs0z0095z854xegfy7j j202yd0esp8p0za60" to "TEGY6d9mh9pgwvwpgs0z0095z854xegfy7jj202yd0esp8p0za60", "00Q979QSMJ29S7BJT3DDAVC5A0DR5Z05B7N 0QT1RCBQ8FXJPZ6RG" to "00Q979QSMJ29S7BJT3DDAVC5A0DR5Z05B7N0QT1RCBQ8FXJPZ6RG", "Taler NDDCAM9XN4HJZFTBD8V6FNE2FJE8G Y734PJ5AGQMY06C8D4HB3Z0" to "NDDCAM9XN4HJZFTBD8V6FNE2FJE8GY734PJ5AGQMY06C8D4HB3Z0", + "KYCVEEXTBXBEMCS5R64C24GFNQVWBN5R2F9QSQ7PN8QXAP1NG4NG" to "KYCVEEXTBXBEMCS5R64C24GFNQVWBN5R2F9QSQ7PN8QXAP1NG4NG" )) { assertEquals( IncomingSubject(IncomingType.reserve, EddsaPublicKey(key)), parseIncomingSubject(subject) ) } + // Good kyc cases + for ((subject, key) in sequenceOf( + "KYC JW398X85FWPKKMS0EYB6TQ1799RMY5DDXTZ FPW4YC3WJ2DWSJT70" to "JW398X85FWPKKMS0EYB6TQ1799RMY5DDXTZFPW4YC3WJ2DWSJT70" + )) { + assertEquals( + IncomingSubject(IncomingType.kyc, EddsaPublicKey(key)), + parseIncomingSubject(subject) + ) + } } } \ No newline at end of file