Skip to main content

rns_net/
ifac.rs

1//! IFAC (Interface Access Codes) — per-interface cryptographic authentication.
2//!
3//! Matches `Transport.py:894-933` (outbound masking) and `Transport.py:1241-1303`
4//! (inbound unmasking). Key derivation matches `Reticulum.py:811-829`.
5
6use rns_crypto::hkdf;
7use rns_crypto::identity::Identity;
8use rns_crypto::sha256;
9
10/// IFAC salt from `Reticulum.py:152`.
11pub const IFAC_SALT: [u8; 32] = [
12    0xad, 0xf5, 0x4d, 0x88, 0x2c, 0x9a, 0x9b, 0x80, 0x77, 0x1e, 0xb4, 0x99, 0x5d, 0x70, 0x2d, 0x4a,
13    0x3e, 0x73, 0x33, 0x91, 0xb2, 0xa0, 0xf5, 0x3f, 0x41, 0x6d, 0x9f, 0x90, 0x7e, 0x55, 0xcf, 0xf8,
14];
15
16pub const IFAC_MIN_SIZE: usize = 1;
17
18/// Pre-computed IFAC state for an interface.
19pub struct IfacState {
20    pub size: usize,
21    pub key: [u8; 64],
22    pub identity: Identity,
23}
24
25/// Derive IFAC state from network name and/or passphrase.
26///
27/// Matches Python `Reticulum.py:811-829`:
28/// ```text
29/// ifac_origin = SHA256(netname) || SHA256(netkey)
30/// ifac_origin_hash = SHA256(ifac_origin)
31/// ifac_key = hkdf(length=64, derive_from=ifac_origin_hash, salt=IFAC_SALT)
32/// ifac_identity = Identity.from_bytes(ifac_key)
33/// ```
34pub fn derive_ifac(
35    netname: Option<&str>,
36    netkey: Option<&str>,
37    size: usize,
38) -> Result<IfacState, String> {
39    let mut ifac_origin = Vec::new();
40
41    if let Some(name) = netname {
42        let hash = sha256::sha256(name.as_bytes());
43        ifac_origin.extend_from_slice(&hash);
44    }
45
46    if let Some(key) = netkey {
47        let hash = sha256::sha256(key.as_bytes());
48        ifac_origin.extend_from_slice(&hash);
49    }
50
51    let ifac_origin_hash = sha256::sha256(&ifac_origin);
52    let ifac_key_vec = hkdf::hkdf(64, &ifac_origin_hash, Some(&IFAC_SALT), None)
53        .map_err(|err| format!("failed to derive IFAC key: {}", err))?;
54
55    let mut ifac_key = [0u8; 64];
56    ifac_key.copy_from_slice(&ifac_key_vec);
57
58    let identity = Identity::from_private_key(&ifac_key);
59
60    Ok(IfacState {
61        size: size.max(IFAC_MIN_SIZE),
62        key: ifac_key,
63        identity,
64    })
65}
66
67/// Mask an outbound packet. Returns new packet with IFAC inserted and masked.
68///
69/// Matches `Transport.py:894-930`:
70/// 1. `ifac = identity.sign(raw)[-ifac_size:]`
71/// 2. `mask = hkdf(length=len(raw)+ifac_size, derive_from=ifac, salt=ifac_key)`
72/// 3. New packet: `[flags|0x80, hops] + ifac + raw[2:]`
73/// 4. XOR mask: flags byte masked BUT 0x80 forced on; hops masked; IFAC NOT masked; payload masked
74pub fn mask_outbound(raw: &[u8], state: &IfacState) -> Vec<u8> {
75    if raw.len() < 2 {
76        return raw.to_vec();
77    }
78
79    // Calculate IFAC: last `size` bytes of the Ed25519 signature
80    let sig = match state.identity.sign(raw) {
81        Ok(sig) => sig,
82        Err(err) => {
83            log::warn!("failed to sign outbound IFAC packet: {}", err);
84            return raw.to_vec();
85        }
86    };
87    let ifac = &sig[64 - state.size..];
88
89    // Generate mask
90    let mask = match hkdf::hkdf(raw.len() + state.size, ifac, Some(&state.key), None) {
91        Ok(mask) => mask,
92        Err(err) => {
93            log::warn!("failed to derive outbound IFAC mask: {}", err);
94            return raw.to_vec();
95        }
96    };
97
98    // Build new_raw: [flags|0x80, hops] + ifac + raw[2..]
99    let mut new_raw = Vec::with_capacity(raw.len() + state.size);
100    new_raw.push(raw[0] | 0x80); // Set IFAC flag
101    new_raw.push(raw[1]);
102    new_raw.extend_from_slice(ifac);
103    new_raw.extend_from_slice(&raw[2..]);
104
105    // Apply mask
106    let mut masked = Vec::with_capacity(new_raw.len());
107    for (i, &byte) in new_raw.iter().enumerate() {
108        if i == 0 {
109            // Mask first header byte, but force IFAC flag on
110            masked.push((byte ^ mask[i]) | 0x80);
111        } else if i == 1 || i > state.size + 1 {
112            // Mask second header byte and payload (after IFAC)
113            masked.push(byte ^ mask[i]);
114        } else {
115            // Don't mask the IFAC itself (positions 2..2+ifac_size)
116            masked.push(byte);
117        }
118    }
119
120    masked
121}
122
123/// Unmask an inbound packet. Returns original packet without IFAC, or None if invalid.
124///
125/// Matches `Transport.py:1241-1303`.
126pub fn unmask_inbound(raw: &[u8], state: &IfacState) -> Option<Vec<u8>> {
127    // Check minimum length
128    if raw.len() <= 2 + state.size {
129        return None;
130    }
131
132    // Check IFAC flag
133    if raw[0] & 0x80 != 0x80 {
134        return None;
135    }
136
137    // Extract IFAC
138    let ifac = &raw[2..2 + state.size];
139
140    // Generate mask
141    let mask = match hkdf::hkdf(raw.len(), ifac, Some(&state.key), None) {
142        Ok(mask) => mask,
143        Err(err) => {
144            log::warn!("failed to derive inbound IFAC mask: {}", err);
145            return None;
146        }
147    };
148
149    // Unmask: header bytes and payload are unmasked, IFAC is left as-is
150    let mut unmasked = Vec::with_capacity(raw.len());
151    for (i, &byte) in raw.iter().enumerate() {
152        if i <= 1 || i > state.size + 1 {
153            // Unmask header bytes and payload
154            unmasked.push(byte ^ mask[i]);
155        } else {
156            // Don't unmask IFAC itself
157            unmasked.push(byte);
158        }
159    }
160
161    // Clear IFAC flag
162    let flags_cleared = unmasked[0] & 0x7F;
163    let hops = unmasked[1];
164
165    // Re-assemble packet without IFAC
166    let mut new_raw = Vec::with_capacity(raw.len() - state.size);
167    new_raw.push(flags_cleared);
168    new_raw.push(hops);
169    new_raw.extend_from_slice(&unmasked[2 + state.size..]);
170
171    // Verify IFAC: expected = identity.sign(new_raw)[-ifac_size:]
172    let expected_sig = match state.identity.sign(&new_raw) {
173        Ok(sig) => sig,
174        Err(err) => {
175            log::warn!("failed to verify inbound IFAC packet: {}", err);
176            return None;
177        }
178    };
179    let expected_ifac = &expected_sig[64 - state.size..];
180
181    if ifac == expected_ifac {
182        Some(new_raw)
183    } else {
184        None
185    }
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191
192    #[test]
193    fn derive_ifac_netname_only() {
194        let state = derive_ifac(Some("testnet"), None, 8).unwrap();
195        assert_eq!(state.size, 8);
196        assert_eq!(state.key.len(), 64);
197        // Identity should be constructable
198        assert!(state.identity.get_private_key().is_some());
199    }
200
201    #[test]
202    fn derive_ifac_netkey_only() {
203        let state = derive_ifac(None, Some("secretpassword"), 16).unwrap();
204        assert_eq!(state.size, 16);
205        assert!(state.identity.get_private_key().is_some());
206    }
207
208    #[test]
209    fn derive_ifac_both() {
210        let state = derive_ifac(Some("testnet"), Some("mypassword"), 8).unwrap();
211        assert_eq!(state.size, 8);
212        // Verify deterministic: same inputs → same key
213        let state2 = derive_ifac(Some("testnet"), Some("mypassword"), 8).unwrap();
214        assert_eq!(state.key, state2.key);
215    }
216
217    #[test]
218    fn mask_unmask_roundtrip() {
219        let state = derive_ifac(Some("testnet"), Some("password"), 8).unwrap();
220
221        // Create a fake packet (flags + hops + 32 bytes payload)
222        let mut raw = vec![0x00, 0x01]; // flags=0, hops=1
223        raw.extend_from_slice(&[0x42u8; 32]);
224
225        let masked = mask_outbound(&raw, &state);
226        assert_ne!(masked, raw);
227        assert!(masked.len() > raw.len()); // IFAC bytes added
228
229        let recovered = unmask_inbound(&masked, &state).expect("unmask should succeed");
230        assert_eq!(recovered, raw);
231    }
232
233    #[test]
234    fn mask_sets_ifac_flag() {
235        let state = derive_ifac(Some("testnet"), None, 8).unwrap();
236
237        let raw = vec![0x00, 0x01, 0x42, 0x43, 0x44, 0x45];
238        let masked = mask_outbound(&raw, &state);
239
240        // First byte should have 0x80 set
241        assert_eq!(masked[0] & 0x80, 0x80);
242    }
243
244    #[test]
245    fn unmask_rejects_bad_ifac() {
246        let state = derive_ifac(Some("testnet"), Some("password"), 8).unwrap();
247
248        let mut raw = vec![0x00, 0x01];
249        raw.extend_from_slice(&[0x42u8; 32]);
250
251        let mut masked = mask_outbound(&raw, &state);
252
253        // Tamper with IFAC bytes (positions 2..10)
254        masked[3] ^= 0xFF;
255
256        let result = unmask_inbound(&masked, &state);
257        assert!(result.is_none());
258    }
259
260    #[test]
261    fn unmask_rejects_missing_flag() {
262        let state = derive_ifac(Some("testnet"), None, 8).unwrap();
263
264        // Packet without 0x80 flag
265        let raw = vec![
266            0x00, 0x01, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x50,
267        ];
268        let result = unmask_inbound(&raw, &state);
269        assert!(result.is_none());
270    }
271
272    #[test]
273    fn unmask_rejects_too_short() {
274        let state = derive_ifac(Some("testnet"), None, 8).unwrap();
275
276        // Packet too short: only 2 + 7 bytes (need at least 2 + ifac_size + 1)
277        let raw = vec![0x80, 0x01, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48];
278        let result = unmask_inbound(&raw, &state);
279        assert!(result.is_none());
280    }
281}