Skip to main content

saorsa_core/fwid/
mod.rs

1// Copyright 2024 Saorsa Labs Limited
2//
3// This software is dual-licensed under:
4// - GNU Affero General Public License v3.0 or later (AGPL-3.0-or-later)
5// - Commercial License
6//
7// For AGPL-3.0 license, see LICENSE-AGPL-3.0
8// For commercial licensing, contact: david@saorsalabs.com
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under these licenses is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
14//! Four-word identifier system for human-readable addressing.
15//!
16//! This module provides the foundational four-word addressing system
17//! as specified in the saorsa-core spec.
18
19use anyhow::{Context, Result};
20use blake3::Hasher;
21use serde::{Deserialize, Serialize};
22use std::fmt;
23
24/// Four-word identifier using dictionary v1
25#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
26pub struct FourWordsV1 {
27    /// Indices into the dictionary (4 u16 values)
28    indices: [u16; 4],
29}
30
31/// A 32-byte key derived from four-words
32#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
33pub struct Key([u8; 32]);
34
35/// Word type alias
36pub type Word = String;
37
38impl FourWordsV1 {
39    /// Create a new FourWordsV1 from indices
40    pub fn new(indices: [u16; 4]) -> Self {
41        Self { indices }
42    }
43
44    /// Get the indices
45    pub fn indices(&self) -> &[u16; 4] {
46        &self.indices
47    }
48}
49
50impl Key {
51    /// Create a new key from bytes
52    pub fn new(bytes: [u8; 32]) -> Self {
53        Self(bytes)
54    }
55
56    /// Get the key bytes
57    pub fn as_bytes(&self) -> &[u8; 32] {
58        &self.0
59    }
60
61    /// Convert to hex string
62    pub fn to_hex(&self) -> String {
63        hex::encode(self.0)
64    }
65
66    /// Create from hex string
67    pub fn from_hex(s: &str) -> Result<Self> {
68        let bytes = hex::decode(s).context("Invalid hex")?;
69        if bytes.len() != 32 {
70            anyhow::bail!("Key must be 32 bytes");
71        }
72        let mut arr = [0u8; 32];
73        arr.copy_from_slice(&bytes);
74        Ok(Self(arr))
75    }
76}
77
78impl From<[u8; 32]> for Key {
79    fn from(value: [u8; 32]) -> Self {
80        Key(value)
81    }
82}
83
84impl From<Key> for [u8; 32] {
85    fn from(value: Key) -> Self {
86        value.0
87    }
88}
89
90impl fmt::Display for Key {
91    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
92        write!(f, "{}", self.to_hex())
93    }
94}
95
96/// Check if four words are valid (exist in dictionary)
97pub fn fw_check(words: [Word; 4]) -> bool {
98    // Delegate validation to the four-word-networking crate to avoid
99    // re-implementing dictionary/encoding logic.
100    let enc = four_word_networking::FourWordEncoding::new(
101        words[0].clone(),
102        words[1].clone(),
103        words[2].clone(),
104        words[3].clone(),
105    );
106    four_word_networking::FourWordEncoder::new()
107        .decode_ipv4(&enc)
108        .is_ok()
109}
110
111/// Convert four words to a key using BLAKE3
112pub fn fw_to_key(words: [Word; 4]) -> Result<Key> {
113    // Validate words first
114    if !fw_check(words.clone()) {
115        anyhow::bail!("Invalid four-words");
116    }
117
118    // Join words and hash
119    let joined = words.join("-");
120    let mut hasher = Hasher::new();
121    hasher.update(joined.as_bytes());
122    let hash = hasher.finalize();
123
124    Ok(Key(*hash.as_bytes()))
125}
126
127/// Compute key from a context string and content
128pub fn compute_key(context: &str, content: &[u8]) -> Key {
129    let mut hasher = Hasher::new();
130    hasher.update(context.as_bytes());
131    hasher.update(content);
132    let hash = hasher.finalize();
133    Key(*hash.as_bytes())
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139
140    #[test]
141    fn test_fw_check() {
142        // The four-word-networking crate has a specific dictionary
143        // We need to use valid dictionary words that encode to an IPv4
144        // For testing, just verify the function doesn't panic
145        let test_words = [
146            "word1".to_string(),
147            "word2".to_string(),
148            "word3".to_string(),
149            "word4".to_string(),
150        ];
151        // Don't assert the result since we don't know if these are valid dictionary words
152        let _ = fw_check(test_words);
153
154        // Test with empty strings (definitely invalid)
155        let invalid = [
156            "".to_string(),
157            "".to_string(),
158            "".to_string(),
159            "".to_string(),
160        ];
161        assert!(!fw_check(invalid));
162    }
163
164    #[test]
165    fn test_fw_to_key() {
166        // fw_to_key requires valid dictionary words that pass fw_check
167        // Since we don't know the exact dictionary, we'll test the error case
168        let invalid_words = [
169            "notindictionary1".to_string(),
170            "notindictionary2".to_string(),
171            "notindictionary3".to_string(),
172            "notindictionary4".to_string(),
173        ];
174
175        // This should fail since the words aren't in the dictionary
176        let result = fw_to_key(invalid_words);
177        assert!(result.is_err());
178
179        // Test that if we had valid words, it would be deterministic
180        // For now, we can't test the success case without knowing valid dictionary words
181    }
182
183    #[test]
184    fn test_key_hex() {
185        let bytes = [42u8; 32];
186        let key = Key::new(bytes);
187        let hex = key.to_hex();
188        let recovered = Key::from_hex(&hex).unwrap();
189        assert_eq!(key, recovered);
190    }
191}