commit 77e9bb891753ecc6ef06660b7a339c9e07675ff4
parent 1a6b822592fb2b8879e1afa23fa41204ad65b1a2
Author: Antoine A <>
Date: Sat, 11 Jan 2025 01:13:45 +0100
common: improve KYC subject parsing
Diffstat:
3 files changed, 58 insertions(+), 32 deletions(-)
diff --git a/common/taler-api/Cargo.toml b/common/taler-api/Cargo.toml
@@ -18,8 +18,8 @@ libdeflater = "1.22.0"
ed25519-dalek = { version = "2.1.1", default-features = false }
tokio = { workspace = true, features = ["rt-multi-thread", "macros", "signal"] }
serde.workspace = true
-tracing.workspace= true
-tracing-subscriber.workspace= true
+tracing.workspace = true
+tracing-subscriber.workspace = true
serde_json.workspace = true
axum.workspace = true
url.workspace = true
diff --git a/common/taler-api/benches/subject.rs b/common/taler-api/benches/subject.rs
@@ -1,6 +1,6 @@
/*
This file is part of TALER
- Copyright (C) 2024 Taler Systems SA
+ Copyright (C) 2024-2025 Taler Systems SA
TALER 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
@@ -74,10 +74,10 @@ fn parser(c: &mut Criterion) {
});
}
- bench(c, "real_simple", real_simple);
- bench(c, "real_splitted", real_splitted);
- bench(c, "rng", &randoms);
- bench(c, "chunks", &chunks);
+ bench(c, "subject_real_simple", real_simple);
+ bench(c, "subject_real_splitted", real_splitted);
+ bench(c, "subject_rng", &randoms);
+ bench(c, "subject_chunks", &chunks);
}
criterion_group!(benches, parser);
diff --git a/common/taler-api/src/subject.rs b/common/taler-api/src/subject.rs
@@ -124,13 +124,24 @@ pub fn parse_outgoing(subject: &str) -> Result<OutgoingSubject, OutgoingSubjectE
pub fn parse_incoming_unstructured(subject: &str) -> Option<IncomingSubjectResult> {
// We expect subject to be less than 65KB
assert!(subject.len() <= u16::MAX as usize);
+
+ const KEY_SIZE: usize = 52;
+ const KYC_SIZE: usize = KEY_SIZE + 3;
+
/** Parse an incoming subject */
+ #[inline]
fn parse_single(str: &str) -> Option<Candidate> {
// Check key type
- let (is_kyc, raw) = if let Some(key) = str.strip_prefix("KYC:") {
- (true, key)
- } else {
- (false, str)
+ let (is_kyc, raw) = match str.len() {
+ KEY_SIZE => (false, str),
+ KYC_SIZE => {
+ if let Some(key) = str.strip_prefix("KYC") {
+ (true, key)
+ } else {
+ return None;
+ }
+ }
+ _ => return None,
};
// Check key validity
@@ -156,7 +167,7 @@ pub fn parse_incoming_unstructured(subject: &str) -> Option<IncomingSubjectResul
let mut concatenated = String::new();
let mut part = None;
for (i, b) in subject.as_bytes().iter().enumerate() {
- if matches!(b, b'0'..=b':' | b'A'..=b'Z' | b'a'..=b'z') {
+ if b.is_ascii_alphanumeric() {
if part.is_none() {
part = Some(i);
}
@@ -185,37 +196,36 @@ pub fn parse_incoming_unstructured(subject: &str) -> Option<IncomingSubjectResul
for end in parts[i..].iter() {
let range = start.start..end.end;
// Until they are to long to be a key
- if range.len() > 56 {
+ if range.len() > KYC_SIZE {
break;
}
- // If the slice is the right size for a key (56B with prefix else 54B)
- if range.len() == 52 || range.len() == 56 {
- // Parse the concatenated parts
- let slice =
- unsafe { &concatenated.get_unchecked(start.start as usize..end.end as usize) };
- if let Some(other) = parse_single(slice) {
- // On success update best candidate
- match &mut best {
- Some(best) => {
- if other.quality > best.quality // We prefer high quality keys
+
+ // Parse the concatenated parts
+ // SAFETY: we now end.end <= concatenated.len
+ let slice =
+ unsafe { &concatenated.get_unchecked(start.start as usize..end.end as usize) };
+ if let Some(other) = parse_single(slice) {
+ // On success update best candidate
+ match &mut best {
+ Some(best) => {
+ if other.quality > best.quality // We prefer high quality keys
|| matches!( // We prefer prefixed keys over reserve keys
(&best.subject.ty(), &other.subject.ty()),
(IncomingType::reserve, IncomingType::kyc | IncomingType::wad)
)
- {
- *best = other
- } else if best.subject.key() != other.subject.key() // If keys are different
+ {
+ *best = other
+ } else if best.subject.key() != other.subject.key() // If keys are different
&& best.quality == other.quality // Of same quality
&& !matches!( // And prefixing is diferent
(&best.subject.ty(), &other.subject.ty()),
(IncomingType::kyc | IncomingType::wad, IncomingType::reserve)
)
- {
- return Some(IncomingSubjectResult::Ambiguous);
- }
+ {
+ return Some(IncomingSubjectResult::Ambiguous);
}
- None => best = Some(other),
}
+ None => best = Some(other),
}
}
}
@@ -234,7 +244,7 @@ fn parse() {
for ty in [IncomingType::reserve, IncomingType::kyc] {
let prefix = match ty {
IncomingType::reserve => "",
- IncomingType::kyc => "KYC:",
+ IncomingType::kyc => "KYC",
IncomingType::wad => unreachable!(),
};
let standard = &format!("{prefix}{key}");
@@ -346,7 +356,7 @@ fn parse() {
#[test]
/** Test parsing logic using real cases */
fn real() {
- // Good case
+ // Good reserve case
for (subject, key) in [
(
"Taler TEGY6d9mh9pgwvwpgs0z0095z854xegfy7j j202yd0esp8p0za60",
@@ -360,6 +370,10 @@ fn real() {
"Taler NDDCAM9XN4HJZFTBD8V6FNE2FJE8G Y734PJ5AGQMY06C8D4HB3Z0",
"NDDCAM9XN4HJZFTBD8V6FNE2FJE8GY734PJ5AGQMY06C8D4HB3Z0",
),
+ (
+ "KYCVEEXTBXBEMCS5R64C24GFNQVWBN5R2F9QSQ7PN8QXAP1NG4NG",
+ "KYCVEEXTBXBEMCS5R64C24GFNQVWBN5R2F9QSQ7PN8QXAP1NG4NG",
+ ),
] {
assert_eq!(
Some(IncomingSubjectResult::Success(IncomingSubject::Reserve(
@@ -368,4 +382,16 @@ fn real() {
parse_incoming_unstructured(subject)
)
}
+ // Good kyc case
+ for (subject, key) in [(
+ "KYC JW398X85FWPKKMS0EYB6TQ1799RMY5DDXTZ FPW4YC3WJ2DWSJT70",
+ "JW398X85FWPKKMS0EYB6TQ1799RMY5DDXTZFPW4YC3WJ2DWSJT70",
+ )] {
+ assert_eq!(
+ Some(IncomingSubjectResult::Success(IncomingSubject::Kyc(
+ EddsaPublicKey::from_str(key).unwrap(),
+ ))),
+ parse_incoming_unstructured(subject)
+ )
+ }
}