communitas_core/
identity.rs1use four_word_networking::FourWordAdaptiveEncoder;
17use std::net::SocketAddr;
18use thiserror::Error;
19
20#[derive(Debug, Error)]
22pub enum IdentityError {
23 #[error("Failed to encode identity: {0}")]
24 EncodingFailed(String),
25
26 #[error("Failed to decode identity: {0}")]
27 DecodingFailed(String),
28
29 #[error("Invalid four-word format: {0}")]
30 InvalidFormat(String),
31
32 #[error("Four-word encoder initialization failed: {0}")]
33 EncoderInitFailed(String),
34}
35
36pub type IdentityResult<T> = Result<T, IdentityError>;
37
38pub fn generate_id_words() -> IdentityResult<String> {
55 let encoder = FourWordAdaptiveEncoder::new().map_err(|e| {
57 IdentityError::EncoderInitFailed(format!("Failed to initialize encoder: {}", e))
58 })?;
59
60 let words = encoder.get_random_words(4);
62 if words.len() != 4 {
63 return Err(IdentityError::EncodingFailed(
64 "Failed to generate 4 dictionary words".to_string(),
65 ));
66 }
67
68 Ok(words.join("-"))
69}
70
71pub fn identity_to_seed(identity: &str) -> IdentityResult<[u8; 32]> {
96 if !validate_identity_format(identity) {
99 return Err(IdentityError::InvalidFormat(format!(
100 "Invalid four-word format: expected word-word-word-word, got: {}",
101 identity
102 )));
103 }
104
105 let hash = blake3::hash(identity.as_bytes());
107 Ok(*hash.as_bytes())
108}
109
110pub fn validate_id_words(identity: &str) -> bool {
120 if !validate_identity_format(identity) {
122 return false;
123 }
124
125 let encoder = match FourWordAdaptiveEncoder::new() {
127 Ok(e) => e,
128 Err(_) => return false,
129 };
130
131 for word in identity.split('-') {
133 if !encoder.is_valid_word(word) {
134 return false;
135 }
136 }
137
138 true
139}
140
141pub fn conn_words(addr: &SocketAddr) -> IdentityResult<String> {
163 let encoder = FourWordAdaptiveEncoder::new().map_err(|e| {
165 IdentityError::EncoderInitFailed(format!("Failed to initialize encoder: {}", e))
166 })?;
167
168 let words = encoder.encode(&addr.to_string()).map_err(|e| {
170 IdentityError::EncodingFailed(format!("Failed to encode connection address: {}", e))
171 })?;
172
173 Ok(words)
174}
175
176pub fn conn_from_words(words: &str) -> IdentityResult<SocketAddr> {
199 if !words.contains(' ') && !words.contains('-') {
201 return Err(IdentityError::InvalidFormat(
202 "Connection identity must contain word separators".to_string(),
203 ));
204 }
205
206 let encoder = FourWordAdaptiveEncoder::new().map_err(|e| {
208 IdentityError::EncoderInitFailed(format!("Failed to initialize encoder: {}", e))
209 })?;
210
211 let addr_str = encoder.decode(words).map_err(|e| {
213 IdentityError::DecodingFailed(format!("Failed to decode connection address: {}", e))
214 })?;
215
216 let addr: SocketAddr = addr_str.parse().map_err(|e| {
218 IdentityError::DecodingFailed(format!("Failed to parse decoded address: {}", e))
219 })?;
220
221 Ok(addr)
222}
223
224pub fn validate_identity_format(words: &str) -> bool {
234 let parts: Vec<&str> = words.split('-').collect();
235 parts.len() == 4 && parts.iter().all(|part| !part.is_empty())
236}
237
238pub fn validate_connection_format(words: &str) -> bool {
248 let parts: Vec<&str> = words.split('-').collect();
249 parts.len() >= 4 && parts.iter().all(|part| !part.is_empty())
250}
251
252#[cfg(test)]
253mod tests {
254 use super::*;
255
256 #[test]
257 fn test_generate_id_words() {
258 let identity = generate_id_words().unwrap();
259
260 let parts: Vec<&str> = identity.split('-').collect();
262 assert_eq!(parts.len(), 4);
263
264 for part in parts {
266 assert!(!part.is_empty());
267 }
268
269 assert!(validate_id_words(&identity));
271 }
272
273 #[test]
274 fn test_identity_to_seed_deterministic() {
275 let identity = generate_id_words().unwrap();
277
278 let seed1 = identity_to_seed(&identity).unwrap();
279 let seed2 = identity_to_seed(&identity).unwrap();
280
281 assert_eq!(seed1, seed2);
283 assert_eq!(seed1.len(), 32);
284 }
285
286 #[test]
287 fn test_identity_to_seed_different_identities() {
288 let identity1 = generate_id_words().unwrap();
290 let identity2 = generate_id_words().unwrap();
291
292 if identity1 == identity2 {
294 return;
296 }
297
298 let seed1 = identity_to_seed(&identity1).unwrap();
299 let seed2 = identity_to_seed(&identity2).unwrap();
300
301 assert_ne!(seed1, seed2);
303 }
304
305 #[test]
306 fn test_validate_id_words() {
307 let valid_identity = generate_id_words().unwrap();
309 assert!(validate_id_words(&valid_identity));
310
311 assert!(!validate_id_words("only-three-words"));
313 assert!(!validate_id_words("too-many-words-here-now"));
314 assert!(!validate_id_words(""));
315 }
316
317 #[test]
318 fn test_conn_words_ipv4() {
319 let addr: SocketAddr = "127.0.0.1:8080".parse().unwrap();
320 let conn_id = conn_words(&addr).unwrap();
321
322 assert!(conn_id.contains(' '));
324
325 let parts: Vec<&str> = conn_id.split(' ').collect();
327 assert!(parts.len() >= 4);
328 }
329
330 #[test]
331 fn test_conn_words_ipv6() {
332 let addr: SocketAddr = "[::1]:8080".parse().unwrap();
333 let conn_id = conn_words(&addr).unwrap();
334
335 assert!(conn_id.contains(' '));
337
338 let parts: Vec<&str> = conn_id.split(' ').collect();
340 assert!(parts.len() >= 4);
341 }
342
343 #[test]
344 fn test_conn_roundtrip_ipv4() {
345 let original: SocketAddr = "192.168.1.100:9000".parse().unwrap();
346
347 let words = conn_words(&original).unwrap();
348 let decoded = conn_from_words(&words).unwrap();
349
350 assert_eq!(original, decoded);
351 }
352
353 #[test]
354 fn test_conn_roundtrip_ipv6() {
355 let original: SocketAddr = "[2001:db8::1]:9000".parse().unwrap();
356
357 let words = conn_words(&original).unwrap();
358 let decoded = conn_from_words(&words).unwrap();
359
360 assert_eq!(original, decoded);
361 }
362
363 #[test]
364 fn test_conn_from_words_invalid_format() {
365 let result = conn_from_words("invalid");
366 assert!(result.is_err());
367 }
368
369 #[test]
370 fn test_validate_identity_format() {
371 assert!(validate_identity_format("ocean-forest-moon-star"));
372 assert!(validate_identity_format("river-mountain-sun-cloud"));
373
374 assert!(!validate_identity_format("only-three-words"));
375 assert!(!validate_identity_format("too-many-words-here-now"));
376 assert!(!validate_identity_format("no spaces allowed"));
377 assert!(!validate_identity_format(""));
378 }
379
380 #[test]
381 fn test_validate_connection_format() {
382 assert!(validate_connection_format("ocean-forest-moon-star"));
383 assert!(validate_connection_format("ocean-forest-moon-star-extra"));
384 assert!(validate_connection_format(
385 "ocean-forest-moon-star-extra-more"
386 ));
387
388 assert!(!validate_connection_format("only-three"));
389 assert!(!validate_connection_format("no spaces"));
390 assert!(!validate_connection_format(""));
391 }
392
393 #[test]
394 fn test_conn_words_deterministic() {
395 let addr: SocketAddr = "10.0.0.1:5000".parse().unwrap();
396
397 let words1 = conn_words(&addr).unwrap();
398 let words2 = conn_words(&addr).unwrap();
399
400 assert_eq!(words1, words2);
401 }
402
403 #[test]
404 fn test_different_ports_different_words() {
405 let addr1: SocketAddr = "127.0.0.1:8080".parse().unwrap();
406 let addr2: SocketAddr = "127.0.0.1:8081".parse().unwrap();
407
408 let words1 = conn_words(&addr1).unwrap();
409 let words2 = conn_words(&addr2).unwrap();
410
411 assert_ne!(words1, words2);
413 }
414}