Skip to main content

rns_core/link/
mod.rs

1pub mod crypto;
2pub mod handshake;
3pub mod identify;
4pub mod keepalive;
5pub mod types;
6
7use alloc::vec::Vec;
8
9use rns_crypto::ed25519::{Ed25519PrivateKey, Ed25519PublicKey};
10use rns_crypto::token::Token;
11use rns_crypto::x25519::X25519PrivateKey;
12use rns_crypto::Rng;
13
14use crate::constants::{
15    LINK_ECPUBSIZE, LINK_ESTABLISHMENT_TIMEOUT_PER_HOP, LINK_KEEPALIVE_MAX, MTU,
16};
17
18pub use types::{LinkAction, LinkError, LinkId, LinkMode, LinkState, TeardownReason};
19
20use crypto::{create_session_token, link_decrypt, link_encrypt};
21use handshake::{
22    build_linkrequest_data, compute_link_id, pack_rtt, parse_linkrequest_data,
23    perform_key_exchange, unpack_rtt, validate_lrproof,
24};
25use keepalive::{
26    compute_establishment_timeout, compute_keepalive, compute_stale_time, is_establishment_timeout,
27    should_go_stale, should_send_keepalive,
28};
29
30/// The Link Engine manages a single link's lifecycle.
31///
32/// It follows the action-queue model: methods return `Vec<LinkAction>` instead
33/// of performing I/O directly. The caller dispatches actions.
34pub struct LinkEngine {
35    link_id: LinkId,
36    state: LinkState,
37    is_initiator: bool,
38    mode: LinkMode,
39
40    // Ephemeral keys
41    prv: X25519PrivateKey,
42
43    // Peer keys
44    peer_pub_bytes: Option<[u8; 32]>,
45    peer_sig_pub_bytes: Option<[u8; 32]>,
46
47    // Session crypto
48    derived_key: Option<Vec<u8>>,
49    token: Option<Token>,
50
51    // Timing
52    request_time: f64,
53    activated_at: Option<f64>,
54    last_inbound: f64,
55    last_outbound: f64,
56    last_keepalive: f64,
57    last_proof: f64,
58    rtt: Option<f64>,
59    keepalive_interval: f64,
60    stale_time: f64,
61    establishment_timeout: f64,
62
63    // Identity
64    remote_identity: Option<([u8; 16], [u8; 64])>,
65    destination_hash: [u8; 16],
66
67    // MDU
68    mtu: u32,
69    mdu: usize,
70}
71
72impl LinkEngine {
73    /// Create a new initiator-side link engine.
74    ///
75    /// Returns `(engine, linkrequest_data)` — the caller must pack linkrequest_data
76    /// into a LINKREQUEST packet and send it.
77    pub fn new_initiator(
78        dest_hash: &[u8; 16],
79        hops: u8,
80        mode: LinkMode,
81        mtu: Option<u32>,
82        now: f64,
83        rng: &mut dyn Rng,
84    ) -> (Self, Vec<u8>) {
85        let prv = X25519PrivateKey::generate(rng);
86        let pub_bytes = prv.public_key().public_bytes();
87        let sig_prv = Ed25519PrivateKey::generate(rng);
88        let sig_pub_bytes = sig_prv.public_key().public_bytes();
89
90        let request_data = build_linkrequest_data(&pub_bytes, &sig_pub_bytes, mtu, mode);
91
92        let link_mtu = mtu.unwrap_or(MTU as u32);
93
94        let engine = LinkEngine {
95            link_id: [0u8; 16], // will be set after packet is built
96            state: LinkState::Pending,
97            is_initiator: true,
98            mode,
99            prv,
100            peer_pub_bytes: None,
101            peer_sig_pub_bytes: None,
102            derived_key: None,
103            token: None,
104            request_time: now,
105            activated_at: None,
106            last_inbound: now,
107            last_outbound: now,
108            last_keepalive: now,
109            last_proof: 0.0,
110            rtt: None,
111            keepalive_interval: LINK_KEEPALIVE_MAX,
112            stale_time: LINK_KEEPALIVE_MAX * 2.0,
113            establishment_timeout: compute_establishment_timeout(
114                LINK_ESTABLISHMENT_TIMEOUT_PER_HOP,
115                hops,
116            ),
117            remote_identity: None,
118            destination_hash: *dest_hash,
119            mtu: link_mtu,
120            mdu: compute_mdu(link_mtu as usize),
121        };
122
123        (engine, request_data)
124    }
125
126    /// Set link_id from the hashable part of the packed LINKREQUEST packet.
127    ///
128    /// Must be called after packing the LINKREQUEST packet (since link_id depends
129    /// on the packet's hashable part).
130    pub fn set_link_id_from_hashable(&mut self, hashable_part: &[u8], data_len: usize) {
131        let extra = if data_len > LINK_ECPUBSIZE {
132            data_len - LINK_ECPUBSIZE
133        } else {
134            0
135        };
136        self.link_id = compute_link_id(hashable_part, extra);
137    }
138
139    /// Create a new responder-side link engine from an incoming LINKREQUEST.
140    ///
141    /// `owner_sig_prv` / `owner_sig_pub` are the destination's signing keys.
142    /// Returns `(engine, actions)` where actions include the LRPROOF data to send.
143    pub fn new_responder(
144        owner_sig_prv: &Ed25519PrivateKey,
145        owner_sig_pub_bytes: &[u8; 32],
146        linkrequest_data: &[u8],
147        hashable_part: &[u8],
148        dest_hash: &[u8; 16],
149        hops: u8,
150        now: f64,
151        rng: &mut dyn Rng,
152    ) -> Result<(Self, Vec<u8>), LinkError> {
153        let (peer_pub, peer_sig_pub, peer_mtu, mode) = parse_linkrequest_data(linkrequest_data)?;
154
155        let extra = if linkrequest_data.len() > LINK_ECPUBSIZE {
156            linkrequest_data.len() - LINK_ECPUBSIZE
157        } else {
158            0
159        };
160        let link_id = compute_link_id(hashable_part, extra);
161
162        // Generate ephemeral keys for this end
163        let prv = X25519PrivateKey::generate(rng);
164        let pub_bytes = prv.public_key().public_bytes();
165        let sig_pub_bytes = *owner_sig_pub_bytes;
166
167        // Perform ECDH + HKDF
168        let derived_key = perform_key_exchange(&prv, &peer_pub, &link_id, mode)?;
169        let token = create_session_token(&derived_key)?;
170
171        let link_mtu = peer_mtu.unwrap_or(MTU as u32);
172
173        // Build LRPROOF
174        let lrproof_data = handshake::build_lrproof(
175            &link_id,
176            &pub_bytes,
177            &sig_pub_bytes,
178            owner_sig_prv,
179            peer_mtu,
180            mode,
181        );
182
183        let engine = LinkEngine {
184            link_id,
185            state: LinkState::Handshake,
186            is_initiator: false,
187            mode,
188            prv,
189            peer_pub_bytes: Some(peer_pub),
190            peer_sig_pub_bytes: Some(peer_sig_pub),
191            derived_key: Some(derived_key),
192            token: Some(token),
193            request_time: now,
194            activated_at: None,
195            last_inbound: now,
196            last_outbound: now,
197            last_keepalive: now,
198            last_proof: 0.0,
199            rtt: None,
200            keepalive_interval: LINK_KEEPALIVE_MAX,
201            stale_time: LINK_KEEPALIVE_MAX * 2.0,
202            establishment_timeout: compute_establishment_timeout(
203                LINK_ESTABLISHMENT_TIMEOUT_PER_HOP,
204                hops,
205            ),
206            remote_identity: None,
207            destination_hash: *dest_hash,
208            mtu: link_mtu,
209            mdu: compute_mdu(link_mtu as usize),
210        };
211
212        Ok((engine, lrproof_data))
213    }
214
215    /// Handle an incoming LRPROOF (initiator side).
216    ///
217    /// Validates the proof, performs ECDH, derives session key, returns LRRTT data
218    /// to be encrypted and sent.
219    pub fn handle_lrproof(
220        &mut self,
221        proof_data: &[u8],
222        peer_sig_pub_bytes: &[u8; 32],
223        now: f64,
224        rng: &mut dyn Rng,
225    ) -> Result<(Vec<u8>, Vec<LinkAction>), LinkError> {
226        if self.state != LinkState::Pending || !self.is_initiator {
227            return Err(LinkError::InvalidState);
228        }
229
230        let peer_sig_pub = Ed25519PublicKey::from_bytes(peer_sig_pub_bytes);
231
232        let (peer_pub, confirmed_mtu, confirmed_mode) =
233            validate_lrproof(proof_data, &self.link_id, &peer_sig_pub, peer_sig_pub_bytes)?;
234
235        if confirmed_mode != self.mode {
236            return Err(LinkError::UnsupportedMode);
237        }
238
239        self.peer_pub_bytes = Some(peer_pub);
240        self.peer_sig_pub_bytes = Some(*peer_sig_pub_bytes);
241
242        // ECDH + HKDF
243        let derived_key = perform_key_exchange(&self.prv, &peer_pub, &self.link_id, self.mode)?;
244        let token = create_session_token(&derived_key)?;
245
246        self.derived_key = Some(derived_key);
247        self.token = Some(token);
248
249        // Update MTU if confirmed
250        if let Some(mtu) = confirmed_mtu {
251            self.mtu = mtu;
252            self.mdu = compute_mdu(mtu as usize);
253        }
254
255        // Compute RTT and activate
256        let rtt = now - self.request_time;
257        self.rtt = Some(rtt);
258        self.state = LinkState::Active;
259        self.activated_at = Some(now);
260        self.last_inbound = now;
261        self.update_keepalive();
262
263        // Build encrypted LRRTT packet data
264        let rtt_packed = pack_rtt(rtt);
265        let rtt_encrypted = self.encrypt(&rtt_packed, rng)?;
266
267        let mut actions = Vec::new();
268        actions.push(LinkAction::StateChanged {
269            link_id: self.link_id,
270            new_state: LinkState::Active,
271            reason: None,
272        });
273        actions.push(LinkAction::LinkEstablished {
274            link_id: self.link_id,
275            rtt,
276            is_initiator: true,
277        });
278
279        Ok((rtt_encrypted, actions))
280    }
281
282    /// Handle an incoming LRRTT (responder side).
283    ///
284    /// Decrypts the RTT packet, activates the link.
285    pub fn handle_lrrtt(
286        &mut self,
287        encrypted_data: &[u8],
288        now: f64,
289    ) -> Result<Vec<LinkAction>, LinkError> {
290        if self.state != LinkState::Handshake || self.is_initiator {
291            return Err(LinkError::InvalidState);
292        }
293
294        let plaintext = self.decrypt(encrypted_data)?;
295        let initiator_rtt = unpack_rtt(&plaintext).ok_or(LinkError::InvalidData)?;
296
297        let measured_rtt = now - self.request_time;
298        let rtt = if measured_rtt > initiator_rtt {
299            measured_rtt
300        } else {
301            initiator_rtt
302        };
303
304        self.rtt = Some(rtt);
305        self.state = LinkState::Active;
306        self.activated_at = Some(now);
307        self.last_inbound = now;
308        self.update_keepalive();
309
310        let mut actions = Vec::new();
311        actions.push(LinkAction::StateChanged {
312            link_id: self.link_id,
313            new_state: LinkState::Active,
314            reason: None,
315        });
316        actions.push(LinkAction::LinkEstablished {
317            link_id: self.link_id,
318            rtt,
319            is_initiator: false,
320        });
321
322        Ok(actions)
323    }
324
325    /// Encrypt plaintext for transmission over this link.
326    pub fn encrypt(&self, plaintext: &[u8], rng: &mut dyn Rng) -> Result<Vec<u8>, LinkError> {
327        let token = self.token.as_ref().ok_or(LinkError::NoSessionKey)?;
328        Ok(link_encrypt(token, plaintext, rng))
329    }
330
331    /// Decrypt ciphertext received on this link.
332    pub fn decrypt(&self, ciphertext: &[u8]) -> Result<Vec<u8>, LinkError> {
333        let token = self.token.as_ref().ok_or(LinkError::NoSessionKey)?;
334        link_decrypt(token, ciphertext)
335    }
336
337    /// Build LINKIDENTIFY data (encrypted).
338    pub fn build_identify(
339        &self,
340        identity: &rns_crypto::identity::Identity,
341        rng: &mut dyn Rng,
342    ) -> Result<Vec<u8>, LinkError> {
343        if self.state != LinkState::Active {
344            return Err(LinkError::InvalidState);
345        }
346        let plaintext = identify::build_identify_data(identity, &self.link_id)?;
347        self.encrypt(&plaintext, rng)
348    }
349
350    /// Handle incoming LINKIDENTIFY (encrypted data).
351    ///
352    /// Only responders (non-initiators) can receive LINKIDENTIFY (Python: Link.py:1017).
353    pub fn handle_identify(&mut self, encrypted_data: &[u8]) -> Result<Vec<LinkAction>, LinkError> {
354        if self.state != LinkState::Active || self.is_initiator {
355            return Err(LinkError::InvalidState);
356        }
357
358        let plaintext = self.decrypt(encrypted_data)?;
359        let (identity_hash, public_key) =
360            identify::validate_identify_data(&plaintext, &self.link_id)?;
361        self.remote_identity = Some((identity_hash, public_key));
362
363        Ok(alloc::vec![LinkAction::RemoteIdentified {
364            link_id: self.link_id,
365            identity_hash,
366            public_key,
367        }])
368    }
369
370    /// Record that an inbound packet was received (updates timing).
371    ///
372    /// If the link is STALE, recovers to ACTIVE (Python: Link.py:987-988).
373    pub fn record_inbound(&mut self, now: f64) -> Vec<LinkAction> {
374        self.last_inbound = now;
375        if self.state == LinkState::Stale {
376            self.state = LinkState::Active;
377            return alloc::vec![LinkAction::StateChanged {
378                link_id: self.link_id,
379                new_state: LinkState::Active,
380                reason: None,
381            }];
382        }
383        Vec::new()
384    }
385
386    /// Record that a proof was received (updates timing for stale detection).
387    pub fn record_proof(&mut self, now: f64) {
388        self.last_proof = now;
389    }
390
391    /// Record that an outbound packet was sent (updates timing).
392    pub fn record_outbound(&mut self, now: f64, is_keepalive: bool) {
393        self.last_outbound = now;
394        if is_keepalive {
395            self.last_keepalive = now;
396        }
397    }
398
399    /// Periodic tick: check keepalive, stale, timeouts.
400    pub fn tick(&mut self, now: f64) -> Vec<LinkAction> {
401        let mut actions = Vec::new();
402
403        match self.state {
404            LinkState::Pending | LinkState::Handshake => {
405                if is_establishment_timeout(self.request_time, self.establishment_timeout, now) {
406                    self.state = LinkState::Closed;
407                    actions.push(LinkAction::StateChanged {
408                        link_id: self.link_id,
409                        new_state: LinkState::Closed,
410                        reason: Some(TeardownReason::Timeout),
411                    });
412                }
413            }
414            LinkState::Active => {
415                let activated = self.activated_at.unwrap_or(0.0);
416                // Python: max(max(self.last_inbound, self.last_proof), activated_at)
417                let last_inbound = self.last_inbound.max(self.last_proof).max(activated);
418
419                if should_go_stale(last_inbound, self.stale_time, now) {
420                    self.state = LinkState::Stale;
421                    actions.push(LinkAction::StateChanged {
422                        link_id: self.link_id,
423                        new_state: LinkState::Stale,
424                        reason: None,
425                    });
426                }
427            }
428            LinkState::Stale => {
429                // In Python, STALE immediately sends teardown and closes
430                self.state = LinkState::Closed;
431                actions.push(LinkAction::StateChanged {
432                    link_id: self.link_id,
433                    new_state: LinkState::Closed,
434                    reason: Some(TeardownReason::Timeout),
435                });
436            }
437            LinkState::Closed => {}
438        }
439
440        actions
441    }
442
443    /// Check if a keepalive should be sent. Returns true if conditions are met.
444    pub fn needs_keepalive(&self, now: f64) -> bool {
445        if self.state != LinkState::Active {
446            return false;
447        }
448        let activated = self.activated_at.unwrap_or(0.0);
449        let last_inbound = self.last_inbound.max(self.last_proof).max(activated);
450
451        // Only send keepalive when past keepalive interval from last inbound
452        if now < last_inbound + self.keepalive_interval {
453            return false;
454        }
455
456        should_send_keepalive(self.last_keepalive, self.keepalive_interval, now)
457    }
458
459    /// Tear down the link (initiator-initiated close).
460    pub fn teardown(&mut self) -> Vec<LinkAction> {
461        if self.state == LinkState::Closed {
462            return Vec::new();
463        }
464        self.state = LinkState::Closed;
465        let reason = if self.is_initiator {
466            TeardownReason::InitiatorClosed
467        } else {
468            TeardownReason::DestinationClosed
469        };
470        alloc::vec![LinkAction::StateChanged {
471            link_id: self.link_id,
472            new_state: LinkState::Closed,
473            reason: Some(reason),
474        }]
475    }
476
477    /// Handle incoming teardown (remote close).
478    pub fn handle_teardown(&mut self) -> Vec<LinkAction> {
479        if self.state == LinkState::Closed {
480            return Vec::new();
481        }
482        self.state = LinkState::Closed;
483        let reason = if self.is_initiator {
484            TeardownReason::DestinationClosed
485        } else {
486            TeardownReason::InitiatorClosed
487        };
488        alloc::vec![LinkAction::StateChanged {
489            link_id: self.link_id,
490            new_state: LinkState::Closed,
491            reason: Some(reason),
492        }]
493    }
494
495    // --- Queries ---
496
497    pub fn link_id(&self) -> &LinkId {
498        &self.link_id
499    }
500
501    pub fn state(&self) -> LinkState {
502        self.state
503    }
504
505    pub fn rtt(&self) -> Option<f64> {
506        self.rtt
507    }
508
509    pub fn mdu(&self) -> usize {
510        self.mdu
511    }
512
513    pub fn is_initiator(&self) -> bool {
514        self.is_initiator
515    }
516
517    pub fn mode(&self) -> LinkMode {
518        self.mode
519    }
520
521    pub fn remote_identity(&self) -> Option<&([u8; 16], [u8; 64])> {
522        self.remote_identity.as_ref()
523    }
524
525    pub fn destination_hash(&self) -> &[u8; 16] {
526        &self.destination_hash
527    }
528
529    /// Get the derived session key (needed for hole-punch token derivation).
530    pub fn derived_key(&self) -> Option<&[u8]> {
531        self.derived_key.as_deref()
532    }
533
534    pub fn keepalive_interval(&self) -> f64 {
535        self.keepalive_interval
536    }
537
538    /// Update the measured RTT (e.g., after path redirect to a direct link).
539    /// Also recalculates keepalive and stale timers.
540    pub fn set_rtt(&mut self, rtt: f64) {
541        self.rtt = Some(rtt);
542        self.update_keepalive();
543    }
544
545    /// Update the link MTU (e.g., after path redirect to a different interface).
546    pub fn set_mtu(&mut self, mtu: u32) {
547        self.mtu = mtu;
548        self.mdu = compute_mdu(mtu as usize);
549    }
550
551    // --- Internal ---
552
553    fn update_keepalive(&mut self) {
554        if let Some(rtt) = self.rtt {
555            self.keepalive_interval = compute_keepalive(rtt);
556            self.stale_time = compute_stale_time(self.keepalive_interval);
557        }
558    }
559}
560
561/// Compute link MDU from MTU.
562///
563/// MDU = floor((mtu - IFAC_MIN_SIZE - HEADER_MINSIZE - TOKEN_OVERHEAD) / AES128_BLOCKSIZE) * AES128_BLOCKSIZE - 1
564fn compute_mdu(mtu: usize) -> usize {
565    use crate::constants::{AES128_BLOCKSIZE, HEADER_MINSIZE, IFAC_MIN_SIZE, TOKEN_OVERHEAD};
566    let numerator = mtu.saturating_sub(IFAC_MIN_SIZE + HEADER_MINSIZE + TOKEN_OVERHEAD);
567    (numerator / AES128_BLOCKSIZE) * AES128_BLOCKSIZE - 1
568}
569
570#[cfg(test)]
571mod tests {
572    use super::*;
573    use crate::constants::LINK_MDU;
574    use rns_crypto::FixedRng;
575
576    fn make_rng(seed: u8) -> FixedRng {
577        FixedRng::new(&[seed; 128])
578    }
579
580    #[test]
581    fn test_compute_mdu_default() {
582        assert_eq!(compute_mdu(500), LINK_MDU);
583    }
584
585    #[test]
586    fn test_full_handshake() {
587        // Setup: destination identity (for responder)
588        let mut rng_id = make_rng(0x01);
589        let dest_sig_prv = Ed25519PrivateKey::generate(&mut rng_id);
590        let dest_sig_pub_bytes = dest_sig_prv.public_key().public_bytes();
591
592        let dest_hash = [0xDD; 16];
593        let mode = LinkMode::Aes256Cbc;
594
595        // Step 1: Initiator creates link request
596        let mut rng_init = make_rng(0x10);
597        let (mut initiator, request_data) =
598            LinkEngine::new_initiator(&dest_hash, 1, mode, Some(500), 100.0, &mut rng_init);
599        assert_eq!(initiator.state(), LinkState::Pending);
600
601        // Simulate packet packing: build a fake hashable part
602        // In real usage, the caller packs a LINKREQUEST packet and calls set_link_id_from_hashable
603        let mut hashable = Vec::new();
604        hashable.push(0x00); // flags byte (lower nibble)
605        hashable.push(0x00); // hops
606        hashable.extend_from_slice(&dest_hash);
607        hashable.push(0x00); // context
608        hashable.extend_from_slice(&request_data);
609
610        initiator.set_link_id_from_hashable(&hashable, request_data.len());
611        assert_ne!(initiator.link_id(), &[0u8; 16]);
612
613        // Step 2: Responder receives link request
614        let mut rng_resp = make_rng(0x20);
615        let (mut responder, lrproof_data) = LinkEngine::new_responder(
616            &dest_sig_prv,
617            &dest_sig_pub_bytes,
618            &request_data,
619            &hashable,
620            &dest_hash,
621            1,
622            100.5,
623            &mut rng_resp,
624        )
625        .unwrap();
626        assert_eq!(responder.state(), LinkState::Handshake);
627        assert_eq!(responder.link_id(), initiator.link_id());
628
629        // Step 3: Initiator validates LRPROOF
630        let mut rng_lrrtt = make_rng(0x30);
631        let (lrrtt_encrypted, actions) = initiator
632            .handle_lrproof(&lrproof_data, &dest_sig_pub_bytes, 100.8, &mut rng_lrrtt)
633            .unwrap();
634        assert_eq!(initiator.state(), LinkState::Active);
635        assert!(initiator.rtt().is_some());
636        assert_eq!(actions.len(), 2); // StateChanged + LinkEstablished
637
638        // Step 4: Responder handles LRRTT
639        let actions = responder.handle_lrrtt(&lrrtt_encrypted, 101.0).unwrap();
640        assert_eq!(responder.state(), LinkState::Active);
641        assert!(responder.rtt().is_some());
642        assert_eq!(actions.len(), 2);
643    }
644
645    #[test]
646    fn test_encrypt_decrypt_after_handshake() {
647        let mut rng_id = make_rng(0x01);
648        let dest_sig_prv = Ed25519PrivateKey::generate(&mut rng_id);
649        let dest_sig_pub_bytes = dest_sig_prv.public_key().public_bytes();
650        let dest_hash = [0xDD; 16];
651
652        let mut rng_init = make_rng(0x10);
653        let (mut initiator, request_data) = LinkEngine::new_initiator(
654            &dest_hash,
655            1,
656            LinkMode::Aes256Cbc,
657            Some(500),
658            100.0,
659            &mut rng_init,
660        );
661        let mut hashable = Vec::new();
662        hashable.push(0x00);
663        hashable.push(0x00);
664        hashable.extend_from_slice(&dest_hash);
665        hashable.push(0x00);
666        hashable.extend_from_slice(&request_data);
667        initiator.set_link_id_from_hashable(&hashable, request_data.len());
668
669        let mut rng_resp = make_rng(0x20);
670        let (mut responder, lrproof_data) = LinkEngine::new_responder(
671            &dest_sig_prv,
672            &dest_sig_pub_bytes,
673            &request_data,
674            &hashable,
675            &dest_hash,
676            1,
677            100.5,
678            &mut rng_resp,
679        )
680        .unwrap();
681
682        let mut rng_lrrtt = make_rng(0x30);
683        let (lrrtt_encrypted, _) = initiator
684            .handle_lrproof(&lrproof_data, &dest_sig_pub_bytes, 100.8, &mut rng_lrrtt)
685            .unwrap();
686        responder.handle_lrrtt(&lrrtt_encrypted, 101.0).unwrap();
687
688        // Now both sides are ACTIVE — test encrypt/decrypt
689        let mut rng_enc = make_rng(0x40);
690        let plaintext = b"Hello over encrypted link!";
691        let ciphertext = initiator.encrypt(plaintext, &mut rng_enc).unwrap();
692        let decrypted = responder.decrypt(&ciphertext).unwrap();
693        assert_eq!(decrypted, plaintext);
694
695        // And in reverse
696        let mut rng_enc2 = make_rng(0x50);
697        let ciphertext2 = responder.encrypt(b"Reply!", &mut rng_enc2).unwrap();
698        let decrypted2 = initiator.decrypt(&ciphertext2).unwrap();
699        assert_eq!(decrypted2, b"Reply!");
700    }
701
702    #[test]
703    fn test_tick_establishment_timeout() {
704        let mut rng = make_rng(0x10);
705        let dest_hash = [0xDD; 16];
706        let (mut engine, _) =
707            LinkEngine::new_initiator(&dest_hash, 1, LinkMode::Aes256Cbc, None, 100.0, &mut rng);
708        // Timeout = 6.0 + 6.0 * 1 = 12.0s → expires at 112.0
709
710        // Before timeout — no state change
711        let actions = engine.tick(110.0);
712        assert!(actions.is_empty());
713
714        // After timeout
715        let actions = engine.tick(113.0);
716        assert_eq!(actions.len(), 1);
717        assert_eq!(engine.state(), LinkState::Closed);
718    }
719
720    #[test]
721    fn test_tick_stale_and_close() {
722        let mut rng_id = make_rng(0x01);
723        let dest_sig_prv = Ed25519PrivateKey::generate(&mut rng_id);
724        let dest_sig_pub_bytes = dest_sig_prv.public_key().public_bytes();
725        let dest_hash = [0xDD; 16];
726
727        let mut rng_init = make_rng(0x10);
728        let (mut initiator, request_data) = LinkEngine::new_initiator(
729            &dest_hash,
730            1,
731            LinkMode::Aes256Cbc,
732            Some(500),
733            100.0,
734            &mut rng_init,
735        );
736        let mut hashable = Vec::new();
737        hashable.push(0x00);
738        hashable.push(0x00);
739        hashable.extend_from_slice(&dest_hash);
740        hashable.push(0x00);
741        hashable.extend_from_slice(&request_data);
742        initiator.set_link_id_from_hashable(&hashable, request_data.len());
743
744        let mut rng_resp = make_rng(0x20);
745        let (_, lrproof_data) = LinkEngine::new_responder(
746            &dest_sig_prv,
747            &dest_sig_pub_bytes,
748            &request_data,
749            &hashable,
750            &dest_hash,
751            1,
752            100.5,
753            &mut rng_resp,
754        )
755        .unwrap();
756
757        let mut rng_lrrtt = make_rng(0x30);
758        initiator
759            .handle_lrproof(&lrproof_data, &dest_sig_pub_bytes, 100.8, &mut rng_lrrtt)
760            .unwrap();
761        assert_eq!(initiator.state(), LinkState::Active);
762
763        // Advance time past stale_time
764        let stale_time = initiator.stale_time;
765        let actions = initiator.tick(100.8 + stale_time + 1.0);
766        assert_eq!(initiator.state(), LinkState::Stale);
767        assert_eq!(actions.len(), 1);
768
769        // Next tick: STALE → CLOSED
770        let actions = initiator.tick(100.8 + stale_time + 2.0);
771        assert_eq!(initiator.state(), LinkState::Closed);
772        assert_eq!(actions.len(), 1);
773    }
774
775    #[test]
776    fn test_needs_keepalive() {
777        let mut rng_id = make_rng(0x01);
778        let dest_sig_prv = Ed25519PrivateKey::generate(&mut rng_id);
779        let dest_sig_pub_bytes = dest_sig_prv.public_key().public_bytes();
780        let dest_hash = [0xDD; 16];
781
782        let mut rng_init = make_rng(0x10);
783        let (mut initiator, request_data) = LinkEngine::new_initiator(
784            &dest_hash,
785            1,
786            LinkMode::Aes256Cbc,
787            Some(500),
788            100.0,
789            &mut rng_init,
790        );
791        let mut hashable = Vec::new();
792        hashable.push(0x00);
793        hashable.push(0x00);
794        hashable.extend_from_slice(&dest_hash);
795        hashable.push(0x00);
796        hashable.extend_from_slice(&request_data);
797        initiator.set_link_id_from_hashable(&hashable, request_data.len());
798
799        let mut rng_resp = make_rng(0x20);
800        let (_, lrproof_data) = LinkEngine::new_responder(
801            &dest_sig_prv,
802            &dest_sig_pub_bytes,
803            &request_data,
804            &hashable,
805            &dest_hash,
806            1,
807            100.5,
808            &mut rng_resp,
809        )
810        .unwrap();
811
812        let mut rng_lrrtt = make_rng(0x30);
813        initiator
814            .handle_lrproof(&lrproof_data, &dest_sig_pub_bytes, 100.8, &mut rng_lrrtt)
815            .unwrap();
816
817        let ka = initiator.keepalive_interval();
818        // Not yet
819        assert!(!initiator.needs_keepalive(100.8 + ka - 1.0));
820        // Past keepalive
821        assert!(initiator.needs_keepalive(100.8 + ka + 1.0));
822    }
823
824    #[test]
825    fn test_needs_keepalive_responder() {
826        let mut rng_id = make_rng(0x01);
827        let dest_sig_prv = Ed25519PrivateKey::generate(&mut rng_id);
828        let dest_sig_pub_bytes = dest_sig_prv.public_key().public_bytes();
829        let dest_hash = [0xDD; 16];
830
831        let mut rng_init = make_rng(0x10);
832        let (mut initiator, request_data) = LinkEngine::new_initiator(
833            &dest_hash,
834            1,
835            LinkMode::Aes256Cbc,
836            Some(500),
837            100.0,
838            &mut rng_init,
839        );
840        let mut hashable = Vec::new();
841        hashable.push(0x00);
842        hashable.push(0x00);
843        hashable.extend_from_slice(&dest_hash);
844        hashable.push(0x00);
845        hashable.extend_from_slice(&request_data);
846        initiator.set_link_id_from_hashable(&hashable, request_data.len());
847
848        let mut rng_resp = make_rng(0x20);
849        let (mut responder, lrproof_data) = LinkEngine::new_responder(
850            &dest_sig_prv,
851            &dest_sig_pub_bytes,
852            &request_data,
853            &hashable,
854            &dest_hash,
855            1,
856            100.5,
857            &mut rng_resp,
858        )
859        .unwrap();
860
861        let mut rng_lrrtt = make_rng(0x30);
862        let (lrrtt_encrypted, _) = initiator
863            .handle_lrproof(&lrproof_data, &dest_sig_pub_bytes, 100.8, &mut rng_lrrtt)
864            .unwrap();
865        responder.handle_lrrtt(&lrrtt_encrypted, 101.0).unwrap();
866
867        let ka = responder.keepalive_interval();
868        // Responder should also send keepalives
869        assert!(!responder.needs_keepalive(101.0 + ka - 1.0));
870        assert!(responder.needs_keepalive(101.0 + ka + 1.0));
871    }
872
873    #[test]
874    fn test_teardown() {
875        let mut rng = make_rng(0x10);
876        let (mut engine, _) =
877            LinkEngine::new_initiator(&[0xDD; 16], 1, LinkMode::Aes256Cbc, None, 100.0, &mut rng);
878        let actions = engine.teardown();
879        assert_eq!(engine.state(), LinkState::Closed);
880        assert_eq!(actions.len(), 1);
881
882        // Teardown again is no-op
883        let actions = engine.teardown();
884        assert!(actions.is_empty());
885    }
886
887    #[test]
888    fn test_handle_teardown() {
889        let mut rng = make_rng(0x10);
890        let (mut engine, _) =
891            LinkEngine::new_initiator(&[0xDD; 16], 1, LinkMode::Aes256Cbc, None, 100.0, &mut rng);
892        let actions = engine.handle_teardown();
893        assert_eq!(engine.state(), LinkState::Closed);
894        assert_eq!(actions.len(), 1);
895        match &actions[0] {
896            LinkAction::StateChanged { reason, .. } => {
897                assert_eq!(*reason, Some(TeardownReason::DestinationClosed));
898            }
899            _ => panic!("Expected StateChanged"),
900        }
901    }
902
903    #[test]
904    fn test_identify_over_link() {
905        let mut rng_id = make_rng(0x01);
906        let dest_sig_prv = Ed25519PrivateKey::generate(&mut rng_id);
907        let dest_sig_pub_bytes = dest_sig_prv.public_key().public_bytes();
908        let dest_hash = [0xDD; 16];
909
910        let mut rng_init = make_rng(0x10);
911        let (mut initiator, request_data) = LinkEngine::new_initiator(
912            &dest_hash,
913            1,
914            LinkMode::Aes256Cbc,
915            Some(500),
916            100.0,
917            &mut rng_init,
918        );
919        let mut hashable = Vec::new();
920        hashable.push(0x00);
921        hashable.push(0x00);
922        hashable.extend_from_slice(&dest_hash);
923        hashable.push(0x00);
924        hashable.extend_from_slice(&request_data);
925        initiator.set_link_id_from_hashable(&hashable, request_data.len());
926
927        let mut rng_resp = make_rng(0x20);
928        let (mut responder, lrproof_data) = LinkEngine::new_responder(
929            &dest_sig_prv,
930            &dest_sig_pub_bytes,
931            &request_data,
932            &hashable,
933            &dest_hash,
934            1,
935            100.5,
936            &mut rng_resp,
937        )
938        .unwrap();
939
940        let mut rng_lrrtt = make_rng(0x30);
941        let (lrrtt_encrypted, _) = initiator
942            .handle_lrproof(&lrproof_data, &dest_sig_pub_bytes, 100.8, &mut rng_lrrtt)
943            .unwrap();
944        responder.handle_lrrtt(&lrrtt_encrypted, 101.0).unwrap();
945
946        // Create identity to identify with
947        let mut rng_ident = make_rng(0x40);
948        let my_identity = rns_crypto::identity::Identity::new(&mut rng_ident);
949
950        // Initiator identifies itself to responder
951        let mut rng_enc = make_rng(0x50);
952        let identify_encrypted = initiator
953            .build_identify(&my_identity, &mut rng_enc)
954            .unwrap();
955
956        let actions = responder.handle_identify(&identify_encrypted).unwrap();
957        assert_eq!(actions.len(), 1);
958        match &actions[0] {
959            LinkAction::RemoteIdentified {
960                identity_hash,
961                public_key,
962                ..
963            } => {
964                assert_eq!(identity_hash, my_identity.hash());
965                assert_eq!(public_key, &my_identity.get_public_key().unwrap());
966            }
967            _ => panic!("Expected RemoteIdentified"),
968        }
969    }
970
971    #[test]
972    fn test_aes128_mode_handshake() {
973        let mut rng_id = make_rng(0x01);
974        let dest_sig_prv = Ed25519PrivateKey::generate(&mut rng_id);
975        let dest_sig_pub_bytes = dest_sig_prv.public_key().public_bytes();
976        let dest_hash = [0xDD; 16];
977
978        let mut rng_init = make_rng(0x10);
979        let (mut initiator, request_data) = LinkEngine::new_initiator(
980            &dest_hash,
981            1,
982            LinkMode::Aes128Cbc,
983            Some(500),
984            100.0,
985            &mut rng_init,
986        );
987        let mut hashable = Vec::new();
988        hashable.push(0x00);
989        hashable.push(0x00);
990        hashable.extend_from_slice(&dest_hash);
991        hashable.push(0x00);
992        hashable.extend_from_slice(&request_data);
993        initiator.set_link_id_from_hashable(&hashable, request_data.len());
994
995        let mut rng_resp = make_rng(0x20);
996        let (mut responder, lrproof_data) = LinkEngine::new_responder(
997            &dest_sig_prv,
998            &dest_sig_pub_bytes,
999            &request_data,
1000            &hashable,
1001            &dest_hash,
1002            1,
1003            100.5,
1004            &mut rng_resp,
1005        )
1006        .unwrap();
1007
1008        let mut rng_lrrtt = make_rng(0x30);
1009        let (lrrtt_encrypted, _) = initiator
1010            .handle_lrproof(&lrproof_data, &dest_sig_pub_bytes, 100.8, &mut rng_lrrtt)
1011            .unwrap();
1012        responder.handle_lrrtt(&lrrtt_encrypted, 101.0).unwrap();
1013
1014        assert_eq!(initiator.state(), LinkState::Active);
1015        assert_eq!(responder.state(), LinkState::Active);
1016        assert_eq!(initiator.mode(), LinkMode::Aes128Cbc);
1017
1018        // Verify encrypt/decrypt works
1019        let mut rng_enc = make_rng(0x40);
1020        let ct = initiator.encrypt(b"AES128 test", &mut rng_enc).unwrap();
1021        let pt = responder.decrypt(&ct).unwrap();
1022        assert_eq!(pt, b"AES128 test");
1023    }
1024}