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 <[email protected]>
This commit is contained in:
2026-10-17 00:31:16 +09:00
co-authored by Claude Opus 5.5
parent cfab3ab543
commit cee40af622
+9 -1
View File
@@ -47,7 +47,8 @@ impl SecretKey {
s.chars().filter(|c| !c.is_whitespace() && *c != '-').map(|c| c.to_ascii_uppercase()).collect(), 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)?; 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); return Err(CryptoError::BadSecretKey);
} }
let (body, check) = rest.split_at(26); let (body, check) = rest.split_at(26);
@@ -96,6 +97,13 @@ fn decode(s: &str) -> Result<[u8; 16]> {
mod tests { mod tests {
use super::*; 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] #[test]
fn roundtrip() { fn roundtrip() {
for _ in 0..200 { for _ in 0..200 {