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