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