//! Supported local hash algorithms (port of `hashing/hashAlgorithms.js`, same ids and order). use crate::core::error::{CoreError, CoreResult}; use serde_json::{json, Map, Value}; #[derive(Debug, Clone, Copy)] pub struct Xof { pub default_bytes: usize, pub min_bytes: usize, pub max_bytes: usize, } #[derive(Debug, Clone, Copy)] pub struct Algorithm { pub id: &'static str, pub name: &'static str, pub category: &'static str, pub digest_bits: Option, pub xof: Option, pub seed_bits: Option, pub key_bytes: Option, pub random_access: bool, pub parameters: Option<&'static str>, } const fn alg(id: &'static str, name: &'static str, category: &'static str, bits: u32) -> Algorithm { Algorithm { id, name, category, digest_bits: Some(bits), xof: None, seed_bits: None, key_bytes: None, random_access: false, parameters: None } } const fn xof(id: &'static str, name: &'static str, default_bytes: usize) -> Algorithm { Algorithm { digest_bits: None, xof: Some(Xof { default_bytes, min_bytes: 16, max_bytes: 1024 }), ..alg(id, name, "xof", 0) } } const fn seeded(id: &'static str, name: &'static str, bits: u32, seed_bits: u32) -> Algorithm { Algorithm { seed_bits: Some(seed_bits), ..alg(id, name, "fast", bits) } } const fn random(id: &'static str, name: &'static str, bits: u32) -> Algorithm { Algorithm { random_access: true, ..alg(id, name, "fast", bits) } } const fn keyed(id: &'static str, name: &'static str, bits: u32, key_bytes: usize) -> Algorithm { Algorithm { key_bytes: Some(key_bytes), ..alg(id, name, "fast", bits) } } const fn crc(id: &'static str, name: &'static str, bits: u32, parameters: &'static str) -> Algorithm { Algorithm { parameters: Some(parameters), ..alg(id, name, "checksums", bits) } } pub const ALGORITHMS: [Algorithm; 49] = [ alg("sha224", "SHA-224", "legacy", 224), alg("sha256", "SHA-256", "recommended", 256), alg("sha384", "SHA-384", "recommended", 384), alg("sha512", "SHA-512", "recommended", 512), alg("sha512-224", "SHA-512/224", "recommended", 224), alg("sha512-256", "SHA-512/256", "recommended", 256), alg("sha3-224", "SHA3-224", "recommended", 224), alg("sha3-256", "SHA3-256", "recommended", 256), alg("sha3-384", "SHA3-384", "recommended", 384), alg("sha3-512", "SHA3-512", "recommended", 512), xof("shake128", "SHAKE128", 32), xof("shake256", "SHAKE256", 64), alg("blake2b-512", "BLAKE2b-512", "recommended", 512), alg("blake2s-256", "BLAKE2s-256", "recommended", 256), alg("blake3", "BLAKE3", "recommended", 256), xof("kangaroo-twelve", "KangarooTwelve", 32), alg("ripemd160", "RIPEMD-160", "legacy", 160), alg("whirlpool", "Whirlpool", "legacy", 512), alg("tiger", "Tiger", "legacy", 192), alg("tiger2", "Tiger2", "legacy", 192), alg("md5", "MD5", "broken", 128), alg("sha1", "SHA-1", "broken", 160), seeded("xxh32", "XXH32", 32, 32), seeded("xxh64", "XXH64", 64, 64), seeded("xxh3-64", "XXH3-64", 64, 64), seeded("xxh3-128", "XXH3-128", 128, 64), seeded("murmur3-x86-32", "MurmurHash3 x86 32", 32, 32), seeded("murmur3-x86-128", "MurmurHash3 x86 128", 128, 32), seeded("murmur3-x64-128", "MurmurHash3 x64 128", 128, 32), random("cityhash32", "CityHash32", 32), random("cityhash64", "CityHash64", 64), random("cityhash128", "CityHash128", 128), random("farmhash32", "FarmHash32", 32), random("farmhash64", "FarmHash64", 64), random("farmhash128", "FarmHash128", 128), keyed("highwayhash64", "HighwayHash64", 64, 32), keyed("highwayhash128", "HighwayHash128", 128, 32), keyed("highwayhash256", "HighwayHash256", 256, 32), keyed("siphash-2-4", "SipHash-2-4", 64, 16), alg("fnv1-32", "FNV-1 32", "fast", 32), alg("fnv1-64", "FNV-1 64", "fast", 64), alg("fnv1a-32", "FNV-1a 32", "fast", 32), alg("fnv1a-64", "FNV-1a 64", "fast", 64), crc("crc16-ccitt-false", "CRC-16/CCITT-FALSE", 16, "poly=0x1021 init=0xffff refin=false refout=false xorout=0x0000"), crc("crc32", "CRC-32/ISO-HDLC", 32, "poly=0x04c11db7 init=0xffffffff refin=true refout=true xorout=0xffffffff"), crc("crc32c", "CRC-32C/Castagnoli", 32, "poly=0x1edc6f41 init=0xffffffff refin=true refout=true xorout=0xffffffff"), crc("crc64-ecma", "CRC-64/ECMA-182", 64, "poly=0x42f0e1eba9ea3693 init=0 refin=false refout=false xorout=0"), crc("crc64-xz", "CRC-64/XZ", 64, "poly=0x42f0e1eba9ea3693 init=all-ones refin=true refout=true xorout=all-ones"), alg("adler32", "Adler-32", "checksums", 32), ]; pub fn by_id(id: &str) -> Option<&'static Algorithm> { ALGORITHMS.iter().find(|a| a.id == id) } fn category_name(category: &str) -> &'static str { match category { "recommended" => "Recommended / Cryptographic", "xof" => "XOF", "legacy" => "Legacy Cryptographic", "broken" => "Broken / Compatibility only", "fast" => "Fast non-cryptographic", _ => "Checksums", } } pub fn typed(code: &str, message: impl Into) -> CoreError { CoreError::new(code, message) } impl Algorithm { pub fn public(&self) -> Value { let mut map = Map::new(); map.insert("id".into(), json!(self.id)); map.insert("name".into(), json!(self.name)); map.insert("category".into(), json!(self.category)); map.insert("categoryName".into(), json!(category_name(self.category))); map.insert("digestBits".into(), json!(self.digest_bits)); map.insert("backend".into(), json!("rust-core")); map.insert("cryptographic".into(), json!(matches!(self.category, "recommended" | "xof" | "legacy" | "broken"))); map.insert( "securityStatus".into(), json!(match self.category { "broken" => "broken", "fast" | "checksums" => "non-cryptographic", _ => "cryptographic", }), ); map.insert("supportsStreaming".into(), json!(!self.random_access)); if let Some(x) = self.xof { map.insert("xof".into(), json!({ "defaultBytes": x.default_bytes, "minBytes": x.min_bytes, "maxBytes": x.max_bytes })); } if let Some(bits) = self.seed_bits { map.insert("seeded".into(), json!(true)); map.insert("seedBits".into(), json!(bits)); } if let Some(bytes) = self.key_bytes { map.insert("keyed".into(), json!(true)); map.insert("keyBytes".into(), json!(bytes)); } if self.random_access { map.insert("randomAccess".into(), json!(true)); } if let Some(p) = self.parameters { map.insert("parameters".into(), json!(p)); } Value::Object(map) } pub fn output_bytes(&self, request: &Request) -> usize { request.output_bytes.unwrap_or_else(|| self.digest_bits.unwrap_or(0) as usize / 8) } } pub fn public_list() -> Value { Value::Array(ALGORITHMS.iter().map(Algorithm::public).collect()) } /// Normalized algorithm request (`{id, outputBytes?, seed?, keyHex?, defaultKey?}`). #[derive(Debug, Clone, PartialEq)] pub struct Request { pub id: &'static str, pub output_bytes: Option, pub seed: Option, pub key: Option>, pub default_key: bool, } impl Request { pub fn algorithm(&self) -> &'static Algorithm { by_id(self.id).expect("normalized request") } pub fn public(&self) -> Value { let mut map = Map::new(); map.insert("id".into(), json!(self.id)); if let Some(bytes) = self.output_bytes { map.insert("outputBytes".into(), json!(bytes)); } if let Some(seed) = self.seed { map.insert("seed".into(), json!(seed.to_string())); } if let Some(key) = &self.key { map.insert("keyHex".into(), json!(hex::encode(key))); map.insert("defaultKey".into(), json!(self.default_key)); } Value::Object(map) } pub fn simple(id: &'static str) -> Self { Request { id, output_bytes: None, seed: None, key: None, default_key: false } } } fn normalize_seed(value: Option<&Value>, bits: u32) -> CoreResult { let source = match value { None | Some(Value::Null) => return Ok(0), Some(Value::String(s)) => s.trim().to_string(), Some(Value::Number(n)) => n.to_string(), Some(other) => other.to_string(), }; if source.is_empty() { return Ok(0); } let invalid = || typed("HASH_INVALID_SEED", "Seed must be an unsigned decimal or 0x-prefixed hexadecimal integer."); let parsed: u128 = if let Some(hex) = source.strip_prefix("0x").or_else(|| source.strip_prefix("0X")) { if hex.is_empty() || !hex.chars().all(|c| c.is_ascii_hexdigit()) { return Err(invalid()); } let trimmed = hex.trim_start_matches('0'); if trimmed.len() > 32 { return Err(typed("HASH_INVALID_SEED", format!("Seed must fit in {bits} bits."))); } u128::from_str_radix(if trimmed.is_empty() { "0" } else { trimmed }, 16).map_err(|_| invalid())? } else { if !source.chars().all(|c| c.is_ascii_digit()) { return Err(invalid()); } let trimmed = source.trim_start_matches('0'); if trimmed.len() > 38 { return Err(typed("HASH_INVALID_SEED", format!("Seed must fit in {bits} bits."))); } if trimmed.is_empty() { 0 } else { trimmed.parse().map_err(|_| invalid())? } }; if parsed >= (1u128 << bits) { return Err(typed("HASH_INVALID_SEED", format!("Seed must fit in {bits} bits."))); } Ok(parsed as u64) } pub fn normalize_requests(input: &Value) -> CoreResult> { let list = input.as_array().filter(|l| !l.is_empty() && l.len() <= ALGORITHMS.len()).ok_or_else(|| typed("HASH_INVALID_ALGORITHMS", "Select between one and all supported algorithms."))?; let mut seen: Vec<&str> = Vec::new(); let mut out = Vec::new(); for source in list { let id = match source { Value::String(s) => s.as_str(), Value::Object(o) => o.get("id").and_then(Value::as_str).unwrap_or(""), _ => "", }; let algorithm = by_id(id).filter(|a| !seen.contains(&a.id)).ok_or_else(|| typed("HASH_UNSUPPORTED_ALGORITHM", format!("Unsupported or duplicate algorithm: {}", if id.is_empty() { "" } else { id })))?; seen.push(algorithm.id); let mut request = Request::simple(algorithm.id); if let Some(x) = algorithm.xof { let raw = source.get("outputBytes").filter(|v| !v.is_null()); let value = match raw { None => Some(x.default_bytes as f64), Some(Value::Number(n)) => n.as_f64(), Some(Value::String(s)) => if s.trim().is_empty() { Some(0.0) } else { s.trim().parse::().ok() }, Some(Value::Bool(b)) => Some(if *b { 1.0 } else { 0.0 }), _ => None, }; match value { Some(v) if v.fract() == 0.0 && v >= x.min_bytes as f64 && v <= x.max_bytes as f64 => request.output_bytes = Some(v as usize), _ => return Err(typed("HASH_INVALID_XOF_LENGTH", format!("{} output must be {}-{} bytes.", algorithm.name, x.min_bytes, x.max_bytes))), } } if let Some(bits) = algorithm.seed_bits { request.seed = Some(normalize_seed(source.get("seed"), bits)?); } if let Some(bytes) = algorithm.key_bytes { let provided = source.get("keyHex").and_then(|v| match v { Value::String(s) if !s.is_empty() => Some(s.trim().to_lowercase()), Value::Null | Value::Bool(false) => None, Value::String(_) => None, other => Some(other.to_string()), }); let key_hex = provided.clone().unwrap_or_else(|| "00".repeat(bytes)); if key_hex.len() != bytes * 2 || !key_hex.chars().all(|c| c.is_ascii_digit() || ('a'..='f').contains(&c)) { return Err(typed("HASH_INVALID_KEY", format!("{} key must be exactly {bytes} bytes in hexadecimal.", algorithm.name))); } request.key = Some(hex::decode(&key_hex).map_err(|_| typed("HASH_INVALID_KEY", "Invalid key."))?); request.default_key = provided.is_none(); } out.push(request); } Ok(out) } #[cfg(test)] mod tests { use super::*; #[test] fn normalizes_like_0_1_0() { assert_eq!(ALGORITHMS.len(), 49); let r = normalize_requests(&json!(["sha256", {"id": "shake128"}, {"id": "xxh64", "seed": "0x10"}, {"id": "siphash-2-4"}])).unwrap(); assert_eq!(r[1].output_bytes, Some(32)); assert_eq!(r[2].seed, Some(16)); assert_eq!(r[3].key.as_deref(), Some(&[0u8; 16][..])); assert!(r[3].default_key); assert_eq!(normalize_requests(&json!(["sha256", "sha256"])).unwrap_err().code, "HASH_UNSUPPORTED_ALGORITHM"); assert_eq!(normalize_requests(&json!([])).unwrap_err().code, "HASH_INVALID_ALGORITHMS"); assert_eq!(normalize_requests(&json!([{"id": "xxh32", "seed": "4294967296"}])).unwrap_err().code, "HASH_INVALID_SEED"); assert_eq!(normalize_requests(&json!([{"id": "xxh32", "seed": "-1"}])).unwrap_err().code, "HASH_INVALID_SEED"); assert_eq!(normalize_requests(&json!([{"id": "shake256", "outputBytes": 8}])).unwrap_err().code, "HASH_INVALID_XOF_LENGTH"); assert_eq!(normalize_requests(&json!([{"id": "highwayhash64", "keyHex": "zz"}])).unwrap_err().code, "HASH_INVALID_KEY"); let list = public_list(); assert_eq!(list[0]["categoryName"], "Legacy Cryptographic"); assert_eq!(list[10]["xof"]["defaultBytes"], 32); assert!(list[0].get("xof").is_none()); } }