use std::net::SocketAddr;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum IdentityError {
#[error("Failed to encode identity: {0}")]
EncodingFailed(String),
#[error("Failed to decode identity: {0}")]
DecodingFailed(String),
#[error("Invalid four-word format: {0}")]
InvalidFormat(String),
#[error("Four-word encoder initialization failed: {0}")]
EncoderInitFailed(String),
}
pub type IdentityResult<T> = Result<T, IdentityError>;
pub fn generate_id_words() -> IdentityResult<String> {
let mut buf = [0u8; 16];
getrandom::getrandom(&mut buf)
.map_err(|e| IdentityError::EncodingFailed(format!("RNG failure: {e}")))?;
let hash = blake3::hash(&buf);
let bytes = hash.as_bytes();
let words: Vec<String> = (0..4)
.map(|i| {
let start = i * 4;
let val = u32::from_le_bytes([
bytes[start],
bytes[start + 1],
bytes[start + 2],
bytes[start + 3],
]);
let mut word = String::new();
let mut v = val;
let len = 4 + (v % 5) as usize;
for _ in 0..len {
word.push((b'a' + (v % 26) as u8) as char);
v /= 26;
}
word
})
.collect();
Ok(words.join("-"))
}
pub fn identity_to_seed(identity: &str) -> IdentityResult<[u8; 32]> {
if !validate_identity_format(identity) {
return Err(IdentityError::InvalidFormat(format!(
"Invalid four-word format: expected word-word-word-word, got: {}",
identity
)));
}
let hash = blake3::hash(identity.as_bytes());
Ok(*hash.as_bytes())
}
pub fn validate_id_words(identity: &str) -> bool {
validate_identity_format(identity)
}
pub fn conn_words(addr: &SocketAddr) -> IdentityResult<String> {
Ok(addr.to_string())
}
pub fn conn_from_words(words: &str) -> IdentityResult<SocketAddr> {
let normalized = words.replace('.', "-");
if !normalized.contains(' ') && !normalized.contains('-') && !words.contains(':') {
return Err(IdentityError::InvalidFormat(
"Connection identity must contain word separators or be a valid address".to_string(),
));
}
let addr: SocketAddr = words
.parse()
.map_err(|e| IdentityError::DecodingFailed(format!("Failed to parse address: {e}")))?;
Ok(addr)
}
pub fn validate_identity_format(words: &str) -> bool {
let parts: Vec<&str> = words.split('-').collect();
parts.len() == 4 && parts.iter().all(|part| !part.is_empty())
}
pub fn validate_connection_format(words: &str) -> bool {
let parts: Vec<&str> = words.split('-').collect();
parts.len() >= 4 && parts.iter().all(|part| !part.is_empty())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_generate_id_words() {
let identity = generate_id_words().unwrap();
let parts: Vec<&str> = identity.split('-').collect();
assert_eq!(parts.len(), 4);
for part in parts {
assert!(!part.is_empty());
}
}
#[test]
fn test_identity_to_seed_deterministic() {
let seed1 = identity_to_seed("ocean-forest-moon-star").unwrap();
let seed2 = identity_to_seed("ocean-forest-moon-star").unwrap();
assert_eq!(seed1, seed2);
assert_eq!(seed1.len(), 32);
}
#[test]
fn test_identity_to_seed_different_identities() {
let seed1 = identity_to_seed("ocean-forest-moon-star").unwrap();
let seed2 = identity_to_seed("river-mountain-sun-cloud").unwrap();
assert_ne!(seed1, seed2);
}
#[test]
fn test_validate_id_words() {
assert!(validate_id_words("ocean-forest-moon-star"));
assert!(!validate_id_words("only-three-words"));
assert!(!validate_id_words("too-many-words-here-now"));
assert!(!validate_id_words(""));
}
#[test]
fn test_conn_words_deterministic() {
let addr: SocketAddr = "10.0.0.1:5000".parse().unwrap();
let words1 = conn_words(&addr).unwrap();
let words2 = conn_words(&addr).unwrap();
assert_eq!(words1, words2);
}
#[test]
fn test_validate_identity_format() {
assert!(validate_identity_format("ocean-forest-moon-star"));
assert!(validate_identity_format("river-mountain-sun-cloud"));
assert!(!validate_identity_format("only-three-words"));
assert!(!validate_identity_format("too-many-words-here-now"));
assert!(!validate_identity_format("no spaces allowed"));
assert!(!validate_identity_format(""));
}
#[test]
fn test_validate_connection_format() {
assert!(validate_connection_format("ocean-forest-moon-star"));
assert!(validate_connection_format("ocean-forest-moon-star-extra"));
assert!(!validate_connection_format("only-three"));
assert!(!validate_connection_format("no spaces"));
assert!(!validate_connection_format(""));
}
}