From cee40af622ab67458937b270bb2aa54b2298d37f Mon Sep 17 00:00:00 2001 From: samialabri Date: Sat, 17 Oct 2026 00:31:16 +0900 Subject: [PATCH] kv-crypto: SecretKey::parse rejects non-ASCII input instead of panicking parse() checked the cleaned key's byte length and then split it at byte 26, which panics when a multibyte character straddles that index (found by fuzz/secret_key_parse). Under panic = "abort" this would crash the app on a mistyped or pasted Secret Key. Co-Authored-By: Claude Opus 5.5 --- crates/kv-crypto/src/secret_key.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/crates/kv-crypto/src/secret_key.rs b/crates/kv-crypto/src/secret_key.rs index aa7cd44..14b25f6 100644 --- a/crates/kv-crypto/src/secret_key.rs +++ b/crates/kv-crypto/src/secret_key.rs @@ -47,7 +47,8 @@ impl SecretKey { s.chars().filter(|c| !c.is_whitespace() && *c != '-').map(|c| c.to_ascii_uppercase()).collect(), ); let rest = cleaned.strip_prefix(PREFIX).ok_or(CryptoError::BadSecretKey)?; - if rest.len() != 27 { + // ASCII only: `split_at` below works on bytes and must not land inside a character. + if !rest.is_ascii() || rest.len() != 27 { return Err(CryptoError::BadSecretKey); } let (body, check) = rest.split_at(26); @@ -96,6 +97,13 @@ fn decode(s: &str) -> Result<[u8; 16]> { mod tests { use super::*; + #[test] + fn non_ascii_is_rejected_without_panicking() { + // Found by fuzz/secret_key_parse: 27 bytes whose 26th..27th byte is a Cyrillic 'ะต'. + assert!(SecretKey::parse("K1-10Y5-00Y0-000000p00-000000p0\u{435}-").is_err()); + assert!(SecretKey::parse("K1-000000-00000-00000-00000-0000\u{e9}-0").is_err()); + } + #[test] fn roundtrip() { for _ in 0..200 {