commit 411fbc4e1e12f3a6528feb54b57db72d540b4363
parent af013f998ce68546e1f0a5ec17b29484283dad0e
Author: Antoine A <>
Date: Sat, 11 Jan 2025 01:30:45 +0100
common: improve KYC subject parsing
Diffstat:
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