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