Skip to main content

rns_net/
destination.rs

1//! Application-facing Destination and AnnouncedIdentity types.
2//!
3//! `Destination` is a pure data struct representing a network endpoint.
4//! `AnnouncedIdentity` captures the result of a received announce.
5
6use rns_core::destination::destination_hash;
7use rns_core::types::{DestHash, DestinationType, Direction, IdentityHash, ProofStrategy};
8use rns_crypto::token::Token;
9use rns_crypto::OsRng;
10use rns_crypto::Rng;
11
12/// Errors related to GROUP destination key operations.
13#[derive(Debug, PartialEq)]
14pub enum GroupKeyError {
15    /// No symmetric key has been loaded or generated.
16    NoKey,
17    /// Key must be 32 bytes (AES-128) or 64 bytes (AES-256).
18    InvalidKeyLength,
19    /// Encryption failed.
20    EncryptionFailed,
21    /// Decryption failed (wrong key, tampered data, or invalid format).
22    DecryptionFailed,
23}
24
25impl core::fmt::Display for GroupKeyError {
26    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
27        match self {
28            GroupKeyError::NoKey => write!(f, "No GROUP key loaded"),
29            GroupKeyError::InvalidKeyLength => write!(f, "Key must be 32 or 64 bytes"),
30            GroupKeyError::EncryptionFailed => write!(f, "Encryption failed"),
31            GroupKeyError::DecryptionFailed => write!(f, "Decryption failed"),
32        }
33    }
34}
35
36/// A network destination (endpoint) for sending or receiving packets.
37///
38/// This is a pure data struct with no behavior — all operations
39/// (register, announce, send) are methods on `RnsNode`.
40#[derive(Debug, Clone)]
41pub struct Destination {
42    /// Computed destination hash.
43    pub hash: DestHash,
44    /// Type: Single, Group, or Plain.
45    pub dest_type: DestinationType,
46    /// Direction: In (receiving) or Out (sending).
47    pub direction: Direction,
48    /// Application name (e.g. "echo_app").
49    pub app_name: String,
50    /// Aspects (e.g. ["echo", "request"]).
51    pub aspects: Vec<String>,
52    /// Identity hash of the owner (for SINGLE destinations).
53    pub identity_hash: Option<IdentityHash>,
54    /// Full public key (64 bytes) of the remote peer (for OUT SINGLE destinations).
55    pub public_key: Option<[u8; 64]>,
56    /// Symmetric key for GROUP destinations (32 or 64 bytes).
57    pub group_key: Option<Vec<u8>>,
58    /// How to handle proofs for incoming packets.
59    pub proof_strategy: ProofStrategy,
60}
61
62impl Destination {
63    /// Create an inbound SINGLE destination (for receiving encrypted packets).
64    ///
65    /// `identity_hash` is the local identity that owns this destination.
66    pub fn single_in(app_name: &str, aspects: &[&str], identity_hash: IdentityHash) -> Self {
67        let dh = destination_hash(app_name, aspects, Some(&identity_hash.0));
68        Destination {
69            hash: DestHash(dh),
70            dest_type: DestinationType::Single,
71            direction: Direction::In,
72            app_name: app_name.into(),
73            aspects: aspects.iter().map(|s| s.to_string()).collect(),
74            identity_hash: Some(identity_hash),
75            public_key: None,
76            group_key: None,
77            proof_strategy: ProofStrategy::ProveNone,
78        }
79    }
80
81    /// Create an outbound SINGLE destination (for sending encrypted packets).
82    ///
83    /// `recalled` contains the remote peer's identity data (from announce/recall).
84    pub fn single_out(app_name: &str, aspects: &[&str], recalled: &AnnouncedIdentity) -> Self {
85        let dh = destination_hash(app_name, aspects, Some(&recalled.identity_hash.0));
86        Destination {
87            hash: DestHash(dh),
88            dest_type: DestinationType::Single,
89            direction: Direction::Out,
90            app_name: app_name.into(),
91            aspects: aspects.iter().map(|s| s.to_string()).collect(),
92            identity_hash: Some(recalled.identity_hash),
93            public_key: Some(recalled.public_key),
94            group_key: None,
95            proof_strategy: ProofStrategy::ProveNone,
96        }
97    }
98
99    /// Create a PLAIN destination (unencrypted, no identity).
100    pub fn plain(app_name: &str, aspects: &[&str]) -> Self {
101        let dh = destination_hash(app_name, aspects, None);
102        Destination {
103            hash: DestHash(dh),
104            dest_type: DestinationType::Plain,
105            direction: Direction::In,
106            app_name: app_name.into(),
107            aspects: aspects.iter().map(|s| s.to_string()).collect(),
108            identity_hash: None,
109            public_key: None,
110            group_key: None,
111            proof_strategy: ProofStrategy::ProveNone,
112        }
113    }
114
115    /// Create a GROUP destination (symmetric encryption with pre-shared key).
116    ///
117    /// No identity needed — the hash is based only on app_name + aspects,
118    /// same as PLAIN. All members sharing the same key can encrypt/decrypt.
119    pub fn group(app_name: &str, aspects: &[&str]) -> Self {
120        let dh = destination_hash(app_name, aspects, None);
121        Destination {
122            hash: DestHash(dh),
123            dest_type: DestinationType::Group,
124            direction: Direction::In,
125            app_name: app_name.into(),
126            aspects: aspects.iter().map(|s| s.to_string()).collect(),
127            identity_hash: None,
128            public_key: None,
129            group_key: None,
130            proof_strategy: ProofStrategy::ProveNone,
131        }
132    }
133
134    /// Generate a new random 64-byte symmetric key (AES-256) for this GROUP destination.
135    pub fn create_keys(&mut self) {
136        let mut key = vec![0u8; 64];
137        OsRng.fill_bytes(&mut key);
138        self.group_key = Some(key);
139    }
140
141    /// Load an existing symmetric key for this GROUP destination.
142    ///
143    /// Key must be 32 bytes (AES-128) or 64 bytes (AES-256).
144    pub fn load_private_key(&mut self, key: Vec<u8>) -> Result<(), GroupKeyError> {
145        if key.len() != 32 && key.len() != 64 {
146            return Err(GroupKeyError::InvalidKeyLength);
147        }
148        self.group_key = Some(key);
149        Ok(())
150    }
151
152    /// Retrieve the symmetric key bytes, if set.
153    pub fn get_private_key(&self) -> Option<&[u8]> {
154        self.group_key.as_deref()
155    }
156
157    /// Encrypt plaintext using this destination's GROUP key.
158    pub fn encrypt(&self, plaintext: &[u8]) -> Result<Vec<u8>, GroupKeyError> {
159        let key = self.group_key.as_ref().ok_or(GroupKeyError::NoKey)?;
160        let token = Token::new(key).map_err(|_| GroupKeyError::EncryptionFailed)?;
161        Ok(token.encrypt(plaintext, &mut OsRng))
162    }
163
164    /// Decrypt ciphertext using this destination's GROUP key.
165    pub fn decrypt(&self, ciphertext: &[u8]) -> Result<Vec<u8>, GroupKeyError> {
166        let key = self.group_key.as_ref().ok_or(GroupKeyError::NoKey)?;
167        let token = Token::new(key).map_err(|_| GroupKeyError::DecryptionFailed)?;
168        token.decrypt(ciphertext).map_err(|_| GroupKeyError::DecryptionFailed)
169    }
170
171    /// Set the proof strategy for this destination.
172    pub fn set_proof_strategy(mut self, strategy: ProofStrategy) -> Self {
173        self.proof_strategy = strategy;
174        self
175    }
176}
177
178/// Information about an announced identity, received via announce or recalled from cache.
179#[derive(Debug, Clone)]
180pub struct AnnouncedIdentity {
181    /// Destination hash that was announced.
182    pub dest_hash: DestHash,
183    /// Identity hash (truncated SHA-256 of public key).
184    pub identity_hash: IdentityHash,
185    /// Full public key (X25519 32 bytes + Ed25519 32 bytes).
186    pub public_key: [u8; 64],
187    /// Optional application data included in the announce.
188    pub app_data: Option<Vec<u8>>,
189    /// Number of hops this announce has traveled.
190    pub hops: u8,
191    /// Timestamp when this announce was received.
192    pub received_at: f64,
193}
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198
199    fn test_identity_hash() -> IdentityHash {
200        IdentityHash([0x42; 16])
201    }
202
203    fn test_announced() -> AnnouncedIdentity {
204        AnnouncedIdentity {
205            dest_hash: DestHash([0xAA; 16]),
206            identity_hash: IdentityHash([0x42; 16]),
207            public_key: [0xBB; 64],
208            app_data: Some(b"test_data".to_vec()),
209            hops: 3,
210            received_at: 1234567890.0,
211        }
212    }
213
214    #[test]
215    fn single_in_hash_matches_raw() {
216        let ih = test_identity_hash();
217        let dest = Destination::single_in("echo", &["app"], ih);
218
219        let raw = destination_hash("echo", &["app"], Some(&ih.0));
220        assert_eq!(dest.hash.0, raw);
221        assert_eq!(dest.dest_type, DestinationType::Single);
222        assert_eq!(dest.direction, Direction::In);
223        assert_eq!(dest.app_name, "echo");
224        assert_eq!(dest.aspects, vec!["app".to_string()]);
225        assert_eq!(dest.identity_hash, Some(ih));
226        assert!(dest.public_key.is_none());
227    }
228
229    #[test]
230    fn single_out_from_recalled() {
231        let recalled = test_announced();
232        let dest = Destination::single_out("echo", &["app"], &recalled);
233
234        let raw = destination_hash("echo", &["app"], Some(&recalled.identity_hash.0));
235        assert_eq!(dest.hash.0, raw);
236        assert_eq!(dest.dest_type, DestinationType::Single);
237        assert_eq!(dest.direction, Direction::Out);
238        assert_eq!(dest.public_key, Some([0xBB; 64]));
239    }
240
241    #[test]
242    fn plain_destination() {
243        let dest = Destination::plain("broadcast", &["test"]);
244
245        let raw = destination_hash("broadcast", &["test"], None);
246        assert_eq!(dest.hash.0, raw);
247        assert_eq!(dest.dest_type, DestinationType::Plain);
248        assert!(dest.identity_hash.is_none());
249        assert!(dest.public_key.is_none());
250    }
251
252    #[test]
253    fn destination_deterministic() {
254        let ih = test_identity_hash();
255        let d1 = Destination::single_in("app", &["a", "b"], ih);
256        let d2 = Destination::single_in("app", &["a", "b"], ih);
257        assert_eq!(d1.hash, d2.hash);
258    }
259
260    #[test]
261    fn different_identity_different_hash() {
262        let d1 = Destination::single_in("app", &["a"], IdentityHash([1; 16]));
263        let d2 = Destination::single_in("app", &["a"], IdentityHash([2; 16]));
264        assert_ne!(d1.hash, d2.hash);
265    }
266
267    #[test]
268    fn proof_strategy_builder() {
269        let dest = Destination::plain("app", &["a"])
270            .set_proof_strategy(ProofStrategy::ProveAll);
271        assert_eq!(dest.proof_strategy, ProofStrategy::ProveAll);
272    }
273
274    #[test]
275    fn announced_identity_fields() {
276        let ai = test_announced();
277        assert_eq!(ai.dest_hash, DestHash([0xAA; 16]));
278        assert_eq!(ai.identity_hash, IdentityHash([0x42; 16]));
279        assert_eq!(ai.public_key, [0xBB; 64]);
280        assert_eq!(ai.app_data, Some(b"test_data".to_vec()));
281        assert_eq!(ai.hops, 3);
282        assert_eq!(ai.received_at, 1234567890.0);
283    }
284
285    #[test]
286    fn multiple_aspects() {
287        let dest = Destination::plain("app", &["one", "two", "three"]);
288        assert_eq!(dest.aspects, vec!["one", "two", "three"]);
289    }
290
291    // --- GROUP destination tests ---
292
293    #[test]
294    fn group_destination_hash_deterministic() {
295        let d1 = Destination::group("myapp", &["chat", "room"]);
296        let d2 = Destination::group("myapp", &["chat", "room"]);
297        assert_eq!(d1.hash, d2.hash);
298        assert_eq!(d1.dest_type, DestinationType::Group);
299        assert_eq!(d1.direction, Direction::In);
300        assert!(d1.identity_hash.is_none());
301        assert!(d1.public_key.is_none());
302        assert!(d1.group_key.is_none());
303    }
304
305    #[test]
306    fn group_destination_hash_matches_plain_hash() {
307        let group = Destination::group("broadcast", &["test"]);
308        let plain = Destination::plain("broadcast", &["test"]);
309        // GROUP and PLAIN with same name produce the same hash (no identity component)
310        assert_eq!(group.hash, plain.hash);
311    }
312
313    #[test]
314    fn group_create_keys() {
315        let mut dest = Destination::group("app", &["g"]);
316        assert!(dest.group_key.is_none());
317        dest.create_keys();
318        let key = dest.group_key.as_ref().unwrap();
319        assert_eq!(key.len(), 64);
320        // Key should not be all zeros (astronomically unlikely with real RNG)
321        assert!(key.iter().any(|&b| b != 0));
322    }
323
324    #[test]
325    fn group_load_private_key_64() {
326        let mut dest = Destination::group("app", &["g"]);
327        let key = vec![0x42u8; 64];
328        assert!(dest.load_private_key(key.clone()).is_ok());
329        assert_eq!(dest.get_private_key(), Some(key.as_slice()));
330    }
331
332    #[test]
333    fn group_load_private_key_32() {
334        let mut dest = Destination::group("app", &["g"]);
335        let key = vec![0xAB; 32];
336        assert!(dest.load_private_key(key.clone()).is_ok());
337        assert_eq!(dest.get_private_key(), Some(key.as_slice()));
338    }
339
340    #[test]
341    fn group_load_private_key_invalid_length() {
342        let mut dest = Destination::group("app", &["g"]);
343        assert_eq!(
344            dest.load_private_key(vec![0; 48]),
345            Err(GroupKeyError::InvalidKeyLength)
346        );
347        assert_eq!(
348            dest.load_private_key(vec![0; 16]),
349            Err(GroupKeyError::InvalidKeyLength)
350        );
351    }
352
353    #[test]
354    fn group_encrypt_decrypt_roundtrip() {
355        let mut dest = Destination::group("app", &["secure"]);
356        dest.load_private_key(vec![0x42u8; 64]).unwrap();
357
358        let plaintext = b"Hello, GROUP destination!";
359        let ciphertext = dest.encrypt(plaintext).unwrap();
360        assert_ne!(ciphertext.as_slice(), plaintext);
361        assert!(ciphertext.len() > plaintext.len()); // includes IV + HMAC overhead
362
363        let decrypted = dest.decrypt(&ciphertext).unwrap();
364        assert_eq!(decrypted, plaintext);
365    }
366
367    #[test]
368    fn group_decrypt_wrong_key_fails() {
369        let mut dest1 = Destination::group("app", &["a"]);
370        dest1.load_private_key(vec![0x42u8; 64]).unwrap();
371
372        let mut dest2 = Destination::group("app", &["a"]);
373        dest2.load_private_key(vec![0xBBu8; 64]).unwrap();
374
375        let ciphertext = dest1.encrypt(b"secret").unwrap();
376        assert_eq!(dest2.decrypt(&ciphertext), Err(GroupKeyError::DecryptionFailed));
377    }
378
379    #[test]
380    fn group_encrypt_without_key_fails() {
381        let dest = Destination::group("app", &["a"]);
382        assert_eq!(dest.encrypt(b"test"), Err(GroupKeyError::NoKey));
383        assert_eq!(dest.decrypt(b"test"), Err(GroupKeyError::NoKey));
384    }
385
386    #[test]
387    fn group_key_interop_with_token() {
388        // Encrypt with Token directly, decrypt with Destination (and vice versa)
389        let key = vec![0x42u8; 64];
390
391        let token = Token::new(&key).unwrap();
392        let ciphertext = token.encrypt(b"from token", &mut OsRng);
393
394        let mut dest = Destination::group("app", &["a"]);
395        dest.load_private_key(key.clone()).unwrap();
396        let decrypted = dest.decrypt(&ciphertext).unwrap();
397        assert_eq!(decrypted, b"from token");
398
399        // And the other direction
400        let ciphertext2 = dest.encrypt(b"from dest").unwrap();
401        let decrypted2 = token.decrypt(&ciphertext2).unwrap();
402        assert_eq!(decrypted2, b"from dest");
403    }
404
405    #[test]
406    fn group_encrypt_decrypt_32byte_key() {
407        let mut dest = Destination::group("app", &["aes128"]);
408        dest.load_private_key(vec![0xABu8; 32]).unwrap();
409
410        let plaintext = b"AES-128 mode";
411        let ciphertext = dest.encrypt(plaintext).unwrap();
412        let decrypted = dest.decrypt(&ciphertext).unwrap();
413        assert_eq!(decrypted, plaintext);
414    }
415}