umsh_crypto/
lib.rs

1#![allow(async_fn_in_trait)]
2#![cfg_attr(not(feature = "std"), no_std)]
3
4//! Cryptographic traits and UMSH-specific key/packet operations.
5//!
6//! This crate separates algorithm providers from protocol logic. The low-level
7//! traits such as [`AesProvider`] and [`Sha256Provider`] can be backed either by
8//! software implementations or hardware accelerators, while [`CryptoEngine`]
9//! implements the UMSH-specific derivation and packet-authentication rules.
10//!
11//! # Example
12//!
13//! ```rust
14//! use umsh_crypto::software::{SoftwareAes, SoftwareIdentity, SoftwareSha256};
15//! use umsh_crypto::{CryptoEngine, NodeIdentity};
16//!
17//! let alice = SoftwareIdentity::from_secret_bytes(&[0x11; 32]);
18//! let bob = SoftwareIdentity::from_secret_bytes(&[0x22; 32]);
19//! let shared = alice.shared_secret_with(bob.public_key()).unwrap();
20//! let engine = CryptoEngine::new(SoftwareAes, SoftwareSha256);
21//! let keys = engine.derive_pairwise_keys(&shared);
22//!
23//! assert_ne!(keys.k_enc, [0u8; 16]);
24//! assert_ne!(keys.k_mic, [0u8; 16]);
25//! ```
26
27use core::ops::Range;
28
29use umsh_core::{
30    ChannelId, ChannelKey, ChannelTag, PacketHeader, PacketType, PublicKey, SourceAddrRef,
31    UnsealedPacket, feed_aad,
32};
33use zeroize::{Zeroize, ZeroizeOnDrop};
34
35pub mod replay;
36
37/// AES block-cipher instance used by the protocol engine.
38pub trait AesCipher {
39    /// Encrypt one 16-byte block in place.
40    fn encrypt_block(&self, block: &mut [u8; 16]);
41    /// Decrypt one 16-byte block in place.
42    fn decrypt_block(&self, block: &mut [u8; 16]);
43}
44
45/// Factory for keyed AES cipher instances.
46pub trait AesProvider {
47    /// Concrete cipher type returned by [`new_cipher`](Self::new_cipher).
48    type Cipher: AesCipher;
49
50    /// Create a new AES-128 cipher using `key`.
51    fn new_cipher(&self, key: &[u8; 16]) -> Self::Cipher;
52}
53
54/// SHA-256 and HMAC-SHA-256 provider.
55pub trait Sha256Provider {
56    /// Hash a list of borrowed byte slices as one concatenated message.
57    fn hash(&self, data: &[&[u8]]) -> [u8; 32];
58    /// Compute HMAC-SHA-256 over a list of borrowed byte slices.
59    fn hmac(&self, key: &[u8], data: &[&[u8]]) -> [u8; 32];
60}
61
62/// Raw X25519 shared secret.
63#[derive(Clone, Zeroize, ZeroizeOnDrop)]
64pub struct SharedSecret(pub [u8; 32]);
65
66/// Node identity capable of signing and key agreement.
67pub trait NodeIdentity {
68    type Error;
69
70    /// Return the long-term Ed25519 public key for this identity.
71    fn public_key(&self) -> &PublicKey;
72
73    /// Return the three-byte node hint derived from [`public_key`](Self::public_key).
74    fn hint(&self) -> umsh_core::NodeHint {
75        self.public_key().hint()
76    }
77
78    /// Sign an arbitrary message.
79    async fn sign(&self, message: &[u8]) -> Result<[u8; 64], Self::Error>;
80    /// Perform X25519-style key agreement with a peer public key.
81    async fn agree(&self, peer: &PublicKey) -> Result<SharedSecret, Self::Error>;
82}
83
84/// Protocol-level crypto failures.
85#[derive(Clone, Debug, PartialEq, Eq)]
86pub enum CryptoError {
87    InvalidPublicKey,
88    InvalidSharedSecret,
89    InvalidPacket,
90    AuthenticationFailed,
91}
92
93/// Maximum channel-name length accepted by
94/// [`CryptoEngine::derive_named_channel_key`].
95///
96/// This is an implementation limit of the no-alloc canonicalization buffer,
97/// not a protocol constant; the spec places no length bound on named
98/// channels.
99pub const MAX_CHANNEL_NAME_LEN: usize = 64;
100
101/// Error returned when a channel name cannot be canonicalized for key
102/// derivation (see multicast-channels.md § Named Channels).
103#[derive(Clone, Copy, Debug, PartialEq, Eq)]
104pub enum ChannelNameError {
105    /// The name contains non-ASCII characters. UTF-8 names are deferred by
106    /// the spec because Unicode case-folding is non-trivial.
107    NotAscii,
108    /// The name exceeds [`MAX_CHANNEL_NAME_LEN`].
109    TooLong,
110}
111
112/// Derived pairwise transport keys.
113#[derive(Clone, Zeroize, ZeroizeOnDrop)]
114pub struct PairwiseKeys {
115    pub k_enc: [u8; 16],
116    pub k_mic: [u8; 16],
117}
118
119/// Derived multicast or channel transport keys.
120#[derive(Clone)]
121pub struct DerivedChannelKeys {
122    pub k_enc: [u8; 16],
123    pub k_mic: [u8; 16],
124    pub channel_id: ChannelId,
125}
126
127impl Zeroize for DerivedChannelKeys {
128    fn zeroize(&mut self) {
129        self.k_enc.zeroize();
130        self.k_mic.zeroize();
131    }
132}
133
134impl Drop for DerivedChannelKeys {
135    fn drop(&mut self) {
136        self.zeroize();
137    }
138}
139
140/// Incremental AES-CMAC state.
141pub struct CmacState<C: AesCipher> {
142    cipher: C,
143    state: [u8; 16],
144    buffer: [u8; 16],
145    pos: usize,
146    k1: [u8; 16],
147    k2: [u8; 16],
148}
149
150impl<C: AesCipher> CmacState<C> {
151    /// Initialize a new incremental CMAC state.
152    pub fn new(cipher: C) -> Self {
153        let mut l = [0u8; 16];
154        cipher.encrypt_block(&mut l);
155        let k1 = dbl(&l);
156        let k2 = dbl(&k1);
157        Self {
158            cipher,
159            state: [0u8; 16],
160            buffer: [0u8; 16],
161            pos: 0,
162            k1,
163            k2,
164        }
165    }
166
167    /// Feed additional bytes into the MAC state.
168    pub fn update(&mut self, mut data: &[u8]) {
169        while !data.is_empty() {
170            let space = 16 - self.pos;
171            let take = space.min(data.len());
172            self.buffer[self.pos..self.pos + take].copy_from_slice(&data[..take]);
173            self.pos += take;
174            data = &data[take..];
175            if self.pos == 16 && !data.is_empty() {
176                self.process_buffer();
177            }
178        }
179    }
180
181    /// Finalize and return the full 16-byte CMAC value.
182    pub fn finalize(self) -> [u8; 16] {
183        let this = self;
184        let mut last = [0u8; 16];
185        if this.pos == 16 {
186            last.copy_from_slice(&this.buffer);
187            xor_in_place(&mut last, &this.k1);
188        } else {
189            last[..this.pos].copy_from_slice(&this.buffer[..this.pos]);
190            last[this.pos] = 0x80;
191            xor_in_place(&mut last, &this.k2);
192        }
193
194        xor_in_place(&mut last, &this.state);
195        this.cipher.encrypt_block(&mut last);
196        last
197    }
198
199    fn process_buffer(&mut self) {
200        let mut block = self.buffer;
201        xor_in_place(&mut block, &self.state);
202        self.cipher.encrypt_block(&mut block);
203        self.state = block;
204        self.buffer = [0u8; 16];
205        self.pos = 0;
206    }
207}
208
209/// UMSH protocol crypto engine.
210pub struct CryptoEngine<A: AesProvider, S: Sha256Provider> {
211    aes: A,
212    sha: S,
213}
214
215impl<A: AesProvider, S: Sha256Provider> CryptoEngine<A, S> {
216    /// Create a new engine from algorithm providers.
217    pub fn new(aes: A, sha: S) -> Self {
218        Self { aes, sha }
219    }
220
221    /// Derive stable pairwise encryption and MIC keys from a shared secret.
222    pub fn derive_pairwise_keys(&self, shared_secret: &SharedSecret) -> PairwiseKeys {
223        let mut okm = [0u8; 32];
224        self.hkdf(
225            &shared_secret.0,
226            b"UMSH-PAIRWISE-SALT",
227            b"UMSH-UNICAST-V1",
228            &mut okm,
229        );
230        let mut keys = PairwiseKeys {
231            k_enc: [0u8; 16],
232            k_mic: [0u8; 16],
233        };
234        keys.k_enc.copy_from_slice(&okm[..16]);
235        keys.k_mic.copy_from_slice(&okm[16..32]);
236        okm.zeroize();
237        keys
238    }
239
240    /// Derive the channel identifier from a raw channel key.
241    pub fn derive_channel_id(&self, channel_key: &ChannelKey) -> ChannelId {
242        let mut out = [0u8; 2];
243        self.hkdf(&channel_key.0, b"UMSH-CHAN-ID", b"", &mut out);
244        ChannelId(out)
245    }
246
247    /// Derive three bytes for presentation — a deterministic colour a user
248    /// interface can give a channel.
249    ///
250    /// This is the channel-identifier derivation run one byte longer, so the
251    /// first two bytes are the channel identifier itself: HKDF-Expand emits a
252    /// prefix of the same block either way. Nothing on the wire carries the
253    /// third byte, and nothing depends on it.
254    pub fn derive_channel_tint(&self, channel_key: &ChannelKey) -> [u8; 3] {
255        let mut out = [0u8; 3];
256        self.hkdf(&channel_key.0, b"UMSH-CHAN-ID", b"", &mut out);
257        out
258    }
259
260    /// Derive the channel tag — a local identity wide enough to tell apart two
261    /// channels whose keys derive the same channel identifier.
262    ///
263    /// Same derivation as the channel identifier, run to sixteen bytes, so the
264    /// identifier and the presentation tint are prefixes of it.
265    pub fn derive_channel_tag(&self, channel_key: &ChannelKey) -> ChannelTag {
266        let mut out = [0u8; 16];
267        self.hkdf(&channel_key.0, b"UMSH-CHAN-ID", b"", &mut out);
268        ChannelTag(out)
269    }
270
271    /// Derive multicast transport keys and the channel identifier.
272    pub fn derive_channel_keys(&self, channel_key: &ChannelKey) -> DerivedChannelKeys {
273        let channel_id = self.derive_channel_id(channel_key);
274        let mut info = [0u8; 15];
275        info[..13].copy_from_slice(b"UMSH-MCAST-V1");
276        info[13..15].copy_from_slice(&channel_id.0);
277        let mut okm = [0u8; 32];
278        self.hkdf(&channel_key.0, b"UMSH-MCAST-SALT", &info, &mut okm);
279        let mut derived = DerivedChannelKeys {
280            k_enc: [0u8; 16],
281            k_mic: [0u8; 16],
282            channel_id,
283        };
284        derived.k_enc.copy_from_slice(&okm[..16]);
285        derived.k_mic.copy_from_slice(&okm[16..32]);
286        okm.zeroize();
287        derived
288    }
289
290    /// Combine pairwise and channel keys for blind-unicast payload protection.
291    pub fn derive_blind_keys(
292        &self,
293        pairwise: &PairwiseKeys,
294        channel: &DerivedChannelKeys,
295    ) -> PairwiseKeys {
296        let mut keys = PairwiseKeys {
297            k_enc: [0u8; 16],
298            k_mic: [0u8; 16],
299        };
300        for (dst, (left, right)) in keys
301            .k_enc
302            .iter_mut()
303            .zip(pairwise.k_enc.iter().zip(channel.k_enc.iter()))
304        {
305            *dst = left ^ right;
306        }
307        for (dst, (left, right)) in keys
308            .k_mic
309            .iter_mut()
310            .zip(pairwise.k_mic.iter().zip(channel.k_mic.iter()))
311        {
312            *dst = left ^ right;
313        }
314        keys
315    }
316
317    /// Derive a channel key from a human-readable channel name.
318    ///
319    /// The name is canonicalized first, per the spec (multicast-channels.md
320    /// § Named Channels): it must be ASCII, and ASCII letters are folded to
321    /// lowercase before derivation — so `Public`, `public`, and `PUBLIC` all
322    /// derive the same key. Non-ASCII names are rejected (UTF-8 case-folding
323    /// is deferred by the spec). Names longer than
324    /// [`MAX_CHANNEL_NAME_LEN`] are rejected as an implementation limit of
325    /// the no-alloc canonicalization buffer.
326    pub fn derive_named_channel_key(&self, name: &str) -> Result<ChannelKey, ChannelNameError> {
327        let bytes = name.as_bytes();
328        if bytes.len() > MAX_CHANNEL_NAME_LEN {
329            return Err(ChannelNameError::TooLong);
330        }
331        if !name.is_ascii() {
332            return Err(ChannelNameError::NotAscii);
333        }
334        let mut canonical = [0u8; MAX_CHANNEL_NAME_LEN];
335        for (dst, byte) in canonical.iter_mut().zip(bytes) {
336            *dst = byte.to_ascii_lowercase();
337        }
338        Ok(ChannelKey(
339            self.sha
340                .hmac(b"UMSH-CHANNEL-V1", &[&canonical[..bytes.len()]]),
341        ))
342    }
343
344    /// Seal a unicast or multicast packet in place.
345    pub fn seal_packet(
346        &self,
347        packet: &mut UnsealedPacket<'_>,
348        keys: &PairwiseKeys,
349    ) -> Result<usize, CryptoError> {
350        let header = packet.header().map_err(|_| CryptoError::InvalidPacket)?;
351        let sec_info = header.sec_info.ok_or(CryptoError::InvalidPacket)?;
352        let full_mac = {
353            let bytes = packet.as_bytes();
354            let mut cmac = self.cmac_state(&keys.k_mic);
355            feed_aad(&header, bytes, |chunk| cmac.update(chunk));
356            cmac.update(packet.body());
357            cmac.finalize()
358        };
359
360        let mic_len = sec_info
361            .scf
362            .mic_size()
363            .map_err(|_| CryptoError::InvalidPacket)?
364            .byte_len();
365        packet.mic_slot()[..mic_len].copy_from_slice(&full_mac[..mic_len]);
366
367        if sec_info.scf.encrypted() {
368            let iv = self.build_ctr_iv(
369                &full_mac[..mic_len],
370                &packet.as_bytes()[packet.sec_info_range()],
371            );
372            self.aes_ctr(&keys.k_enc, &iv, packet.body_mut());
373        }
374
375        Ok(mic_len)
376    }
377
378    /// Seal a blind-unicast packet, including its hidden address block.
379    pub fn seal_blind_packet(
380        &self,
381        packet: &mut UnsealedPacket<'_>,
382        blind_keys: &PairwiseKeys,
383        channel_keys: &DerivedChannelKeys,
384    ) -> Result<usize, CryptoError> {
385        let header = packet.header().map_err(|_| CryptoError::InvalidPacket)?;
386        match header.packet_type() {
387            PacketType::BlindUnicast | PacketType::BlindUnicastAckReq => {}
388            _ => return Err(CryptoError::InvalidPacket),
389        }
390
391        let sec_info = header.sec_info.ok_or(CryptoError::InvalidPacket)?;
392        let blind_addr_range = packet
393            .blind_addr_range()
394            .ok_or(CryptoError::InvalidPacket)?;
395        let full_mac = {
396            let bytes = packet.as_bytes();
397            let mut cmac = self.cmac_state(&blind_keys.k_mic);
398            feed_aad(&header, bytes, |chunk| cmac.update(chunk));
399            cmac.update(packet.body());
400            cmac.finalize()
401        };
402
403        let mic_len = sec_info
404            .scf
405            .mic_size()
406            .map_err(|_| CryptoError::InvalidPacket)?
407            .byte_len();
408        let iv = self.build_ctr_iv(
409            &full_mac[..mic_len],
410            &packet.as_bytes()[packet.sec_info_range()],
411        );
412        packet.mic_slot()[..mic_len].copy_from_slice(&full_mac[..mic_len]);
413        if sec_info.scf.encrypted() {
414            self.aes_ctr(&blind_keys.k_enc, &iv, packet.body_mut());
415            self.aes_ctr(
416                &channel_keys.k_enc,
417                &iv,
418                &mut packet.as_bytes_mut()[blind_addr_range],
419            );
420        }
421        Ok(mic_len)
422    }
423
424    /// Verify and, if needed, decrypt a received secure packet in place.
425    pub fn open_packet(
426        &self,
427        buf: &mut [u8],
428        header: &PacketHeader,
429        keys: &PairwiseKeys,
430    ) -> Result<Range<usize>, CryptoError> {
431        let sec_info = header.sec_info.ok_or(CryptoError::InvalidPacket)?;
432        let mut mic = [0u8; 16];
433        let mic_len = header.mic_range.end - header.mic_range.start;
434        mic[..mic_len].copy_from_slice(&buf[header.mic_range.clone()]);
435        if sec_info.scf.encrypted() {
436            let iv = self.build_ctr_iv(&mic[..mic_len], &buf[sec_info_bytes_range(header)?]);
437            self.aes_ctr(&keys.k_enc, &iv, &mut buf[header.body_range.clone()]);
438        }
439
440        let full_mac = {
441            let mut cmac = self.cmac_state(&keys.k_mic);
442            feed_aad(header, buf, |chunk| cmac.update(chunk));
443            cmac.update(&buf[header.body_range.clone()]);
444            cmac.finalize()
445        };
446        if !constant_time_eq(&mic[..mic_len], &full_mac[..mic_len]) {
447            return Err(CryptoError::AuthenticationFailed);
448        }
449
450        let body_range = match (header.packet_type(), header.source) {
451            (PacketType::Multicast, SourceAddrRef::Encrypted { len, .. }) => {
452                (header.body_range.start + len)..header.body_range.end
453            }
454            _ => header.body_range.clone(),
455        };
456        Ok(body_range)
457    }
458
459    /// Decrypt the blinded destination/source address block of a blind unicast.
460    pub fn decrypt_blind_addr(
461        &self,
462        buf: &mut [u8],
463        header: &PacketHeader,
464        channel_keys: &DerivedChannelKeys,
465    ) -> Result<(umsh_core::NodeHint, SourceAddrRef), CryptoError> {
466        match header.source {
467            SourceAddrRef::Encrypted { offset, len } => {
468                let addr_start = offset.checked_sub(3).ok_or(CryptoError::InvalidPacket)?;
469                let addr_end = addr_start + 3 + len;
470                let iv = self.build_ctr_iv(
471                    &buf[header.mic_range.clone()],
472                    &buf[sec_info_bytes_range(header)?],
473                );
474                self.aes_ctr(&channel_keys.k_enc, &iv, &mut buf[addr_start..addr_end]);
475                let dst = umsh_core::NodeHint([
476                    buf[addr_start],
477                    buf[addr_start + 1],
478                    buf[addr_start + 2],
479                ]);
480                let source = if len == 3 {
481                    SourceAddrRef::Hint(umsh_core::NodeHint([
482                        buf[addr_start + 3],
483                        buf[addr_start + 4],
484                        buf[addr_start + 5],
485                    ]))
486                } else if len == 32 {
487                    SourceAddrRef::FullKeyAt {
488                        offset: addr_start + 3,
489                    }
490                } else {
491                    return Err(CryptoError::InvalidPacket);
492                };
493                Ok((dst, source))
494            }
495            SourceAddrRef::Hint(hint) => {
496                let dst = header.dst.ok_or(CryptoError::InvalidPacket)?;
497                Ok((dst, SourceAddrRef::Hint(hint)))
498            }
499            SourceAddrRef::FullKeyAt { offset } => {
500                let dst = header.dst.ok_or(CryptoError::InvalidPacket)?;
501                let _ = buf
502                    .get(offset..offset + 32)
503                    .ok_or(CryptoError::InvalidPacket)?;
504                Ok((dst, SourceAddrRef::FullKeyAt { offset }))
505            }
506            SourceAddrRef::None => Err(CryptoError::InvalidPacket),
507        }
508    }
509
510    /// Compute the 8-byte MAC ack trailer (`ack_mic || ack_tag`) from a full
511    /// CMAC and `k_enc`.
512    ///
513    /// The trailer splits into two 4-byte halves:
514    ///
515    /// - **`ack_mic`** = the first 4 bytes of `full_cmac`. Because the on-wire
516    ///   MIC is a prefix-truncation of the full CMAC, this equals the first
517    ///   4 bytes of the acknowledged packet's on-wire MIC — a *public*
518    ///   correlation handle any node that received the original packet
519    ///   (including forwarding repeaters) can compute.
520    /// - **`ack_tag`** = `truncate_4( AES-128-ECB(k_enc, full_cmac) )`. A keyed
521    ///   value only the original sender and final destination can produce; it
522    ///   authenticates the standalone ack.
523    ///
524    /// See the spec's *Ack Tag Construction* section.
525    pub fn compute_ack_trailer(&self, full_cmac: &[u8; 16], k_enc: &[u8; 16]) -> [u8; 8] {
526        let cipher = self.aes.new_cipher(k_enc);
527        let mut block = *full_cmac;
528        cipher.encrypt_block(&mut block);
529        let mut trailer = [0u8; 8];
530        trailer[..4].copy_from_slice(&full_cmac[..4]); // ack_mic (public)
531        trailer[4..].copy_from_slice(&block[..4]); // ack_tag (keyed)
532        trailer
533    }
534
535    /// Create a reusable incremental CMAC state.
536    pub fn cmac_state(&self, key: &[u8; 16]) -> CmacState<A::Cipher> {
537        CmacState::new(self.aes.new_cipher(key))
538    }
539
540    /// Convenience wrapper for AES-CMAC over concatenated slices.
541    pub fn aes_cmac(&self, key: &[u8; 16], data: &[&[u8]]) -> [u8; 16] {
542        let mut state = self.cmac_state(key);
543        for chunk in data {
544            state.update(chunk);
545        }
546        state.finalize()
547    }
548
549    /// Apply AES-CTR using `iv` as the initial counter block.
550    pub fn aes_ctr(&self, key: &[u8; 16], iv: &[u8; 16], data: &mut [u8]) {
551        let cipher = self.aes.new_cipher(key);
552        let mut counter = *iv;
553        for chunk in data.chunks_mut(16) {
554            let mut stream = counter;
555            cipher.encrypt_block(&mut stream);
556            for (dst, src) in chunk.iter_mut().zip(stream.iter()) {
557                *dst ^= *src;
558            }
559            increment_counter(&mut counter);
560        }
561    }
562
563    /// Construct the CTR IV from MIC bytes and SECINFO bytes.
564    ///
565    /// With a 16-byte MIC the IV is the MIC alone; shorter MICs are
566    /// padded from the SECINFO bytes, so the IV construction of a
567    /// received packet must locate SECINFO exactly (see
568    /// [`sec_info_bytes_range`]).
569    pub fn build_ctr_iv(&self, mic: &[u8], sec_info_bytes: &[u8]) -> [u8; 16] {
570        let mut iv = [0u8; 16];
571        let mut written = 0usize;
572        for byte in mic.iter().chain(sec_info_bytes.iter()).take(16) {
573            iv[written] = *byte;
574            written += 1;
575        }
576        iv
577    }
578
579    /// Run HKDF-SHA256 and write the output into `okm`.
580    pub fn hkdf(&self, ikm: &[u8], salt: &[u8], info: &[u8], okm: &mut [u8]) {
581        let prk = self.sha.hmac(salt, &[ikm]);
582        let mut previous = [0u8; 32];
583        let mut previous_len = 0usize;
584        let mut written = 0usize;
585        let mut counter = 1u8;
586        while written < okm.len() {
587            let next = self
588                .sha
589                .hmac(&prk, &[&previous[..previous_len], info, &[counter]]);
590            let take = (okm.len() - written).min(next.len());
591            okm[written..written + take].copy_from_slice(&next[..take]);
592            previous = next;
593            previous_len = 32;
594            written += take;
595            counter = counter.wrapping_add(1);
596        }
597    }
598}
599
600fn xor_in_place(dst: &mut [u8; 16], rhs: &[u8; 16]) {
601    for (left, right) in dst.iter_mut().zip(rhs.iter()) {
602        *left ^= *right;
603    }
604}
605
606fn dbl(block: &[u8; 16]) -> [u8; 16] {
607    let mut out = [0u8; 16];
608    let mut carry = 0u8;
609    for (index, byte) in block.iter().enumerate().rev() {
610        out[index] = (byte << 1) | carry;
611        carry = byte >> 7;
612    }
613    if block[0] & 0x80 != 0 {
614        out[15] ^= 0x87;
615    }
616    out
617}
618
619/// The on-wire SECINFO byte range of a parsed secure packet. In every
620/// secure layout (unicast, multicast, blind unicast) SECINFO
621/// immediately precedes the options block — not the body, which the
622/// options end marker or a blind address block may separate from it.
623fn sec_info_bytes_range(header: &PacketHeader) -> Result<Range<usize>, CryptoError> {
624    let sec_info = header.sec_info.ok_or(CryptoError::InvalidPacket)?;
625    header
626        .options_range
627        .start
628        .checked_sub(sec_info.wire_len())
629        .map(|start| start..header.options_range.start)
630        .ok_or(CryptoError::InvalidPacket)
631}
632
633fn increment_counter(counter: &mut [u8; 16]) {
634    for byte in counter.iter_mut().rev() {
635        let (next, carry) = byte.overflowing_add(1);
636        *byte = next;
637        if !carry {
638            break;
639        }
640    }
641}
642
643fn constant_time_eq(left: &[u8], right: &[u8]) -> bool {
644    let mut diff = left.len() ^ right.len();
645    let max_len = left.len().max(right.len());
646    for index in 0..max_len {
647        let lhs = left.get(index).copied().unwrap_or(0);
648        let rhs = right.get(index).copied().unwrap_or(0);
649        diff |= usize::from(lhs ^ rhs);
650    }
651    diff == 0
652}
653
654#[cfg(feature = "software-crypto")]
655pub mod software {
656    use aes::cipher::{BlockDecrypt, BlockEncrypt, KeyInit, generic_array::GenericArray};
657    use curve25519_dalek::{edwards::CompressedEdwardsY, montgomery::MontgomeryPoint};
658    use ed25519_dalek::{Signer, SigningKey};
659    use rand_core::CryptoRngCore;
660    use sha2::{Digest, Sha256, Sha512};
661
662    use super::*;
663
664    pub struct SoftwareAes;
665
666    pub struct SoftwareAesCipher(aes::Aes128);
667
668    impl AesCipher for SoftwareAesCipher {
669        fn encrypt_block(&self, block: &mut [u8; 16]) {
670            self.0.encrypt_block(GenericArray::from_mut_slice(block));
671        }
672
673        fn decrypt_block(&self, block: &mut [u8; 16]) {
674            self.0.decrypt_block(GenericArray::from_mut_slice(block));
675        }
676    }
677
678    impl AesProvider for SoftwareAes {
679        type Cipher = SoftwareAesCipher;
680
681        fn new_cipher(&self, key: &[u8; 16]) -> Self::Cipher {
682            SoftwareAesCipher(aes::Aes128::new(GenericArray::from_slice(key)))
683        }
684    }
685
686    pub struct SoftwareSha256;
687
688    impl Sha256Provider for SoftwareSha256 {
689        fn hash(&self, data: &[&[u8]]) -> [u8; 32] {
690            let mut hasher = Sha256::new();
691            for chunk in data {
692                hasher.update(chunk);
693            }
694            hasher.finalize().into()
695        }
696
697        fn hmac(&self, key: &[u8], data: &[&[u8]]) -> [u8; 32] {
698            const BLOCK_LEN: usize = 64;
699            let mut key_block = [0u8; BLOCK_LEN];
700            if key.len() > BLOCK_LEN {
701                key_block[..32].copy_from_slice(&self.hash(&[key]));
702            } else {
703                key_block[..key.len()].copy_from_slice(key);
704            }
705
706            let mut ipad = [0x36u8; BLOCK_LEN];
707            let mut opad = [0x5Cu8; BLOCK_LEN];
708            for index in 0..BLOCK_LEN {
709                ipad[index] ^= key_block[index];
710                opad[index] ^= key_block[index];
711            }
712
713            let mut inner = Sha256::new();
714            inner.update(ipad);
715            for chunk in data {
716                inner.update(chunk);
717            }
718            let inner_hash = inner.finalize();
719
720            let mut outer = Sha256::new();
721            outer.update(opad);
722            outer.update(inner_hash);
723            outer.finalize().into()
724        }
725    }
726
727    /// Cloning is deliberate: a holder that must both hand the identity to
728    /// the MAC and retain signing ability (e.g. standalone node-identity
729    /// bundles) keeps a clone rather than round-tripping secret bytes.
730    #[derive(Clone)]
731    pub struct SoftwareIdentity {
732        secret: SigningKey,
733        public: PublicKey,
734    }
735
736    impl SoftwareIdentity {
737        pub fn generate(rng: &mut impl CryptoRngCore) -> Self {
738            let secret = SigningKey::generate(rng);
739            let public = PublicKey(secret.verifying_key().to_bytes());
740            Self { secret, public }
741        }
742
743        pub fn from_secret_bytes(bytes: &[u8; 32]) -> Self {
744            let secret = SigningKey::from_bytes(bytes);
745            let public = PublicKey(secret.verifying_key().to_bytes());
746            Self { secret, public }
747        }
748
749        pub fn shared_secret_with(&self, peer: &PublicKey) -> Result<SharedSecret, CryptoError> {
750            let local = signing_key_to_x25519(&self.secret);
751            let remote = public_key_to_x25519(peer)?;
752            let shared = local.diffie_hellman(&remote);
753            let bytes = shared.to_bytes();
754            if bytes.iter().all(|byte| *byte == 0) {
755                return Err(CryptoError::InvalidSharedSecret);
756            }
757            Ok(SharedSecret(bytes))
758        }
759    }
760
761    impl NodeIdentity for SoftwareIdentity {
762        type Error = CryptoError;
763
764        fn public_key(&self) -> &PublicKey {
765            &self.public
766        }
767
768        async fn sign(&self, message: &[u8]) -> Result<[u8; 64], Self::Error> {
769            Ok(self.secret.sign(message).to_bytes())
770        }
771
772        async fn agree(&self, peer: &PublicKey) -> Result<SharedSecret, Self::Error> {
773            self.shared_secret_with(peer)
774        }
775    }
776
777    pub type SoftwareCryptoEngine = CryptoEngine<SoftwareAes, SoftwareSha256>;
778
779    fn signing_key_to_x25519(secret: &SigningKey) -> x25519_dalek::StaticSecret {
780        let digest = Sha512::digest(secret.to_bytes());
781        let mut scalar = [0u8; 32];
782        scalar.copy_from_slice(&digest[..32]);
783        scalar[0] &= 248;
784        scalar[31] &= 127;
785        scalar[31] |= 64;
786        x25519_dalek::StaticSecret::from(scalar)
787    }
788
789    fn public_key_to_x25519(public: &PublicKey) -> Result<x25519_dalek::PublicKey, CryptoError> {
790        let compressed = CompressedEdwardsY(public.0);
791        let edwards = compressed
792            .decompress()
793            .ok_or(CryptoError::InvalidPublicKey)?;
794        let mont: MontgomeryPoint = edwards.to_montgomery();
795        Ok(x25519_dalek::PublicKey::from(mont.to_bytes()))
796    }
797
798    /// Return `true` if `pk`'s bytes decode to a valid Ed25519 compressed
799    /// public-key point on the curve. Used by the MAC layer and CLI to reject
800    /// malformed peer keys before they can poison the peer registry.
801    pub fn is_valid_ed25519_public_key(pk: &PublicKey) -> bool {
802        CompressedEdwardsY(pk.0).decompress().is_some()
803    }
804
805    /// Verify a detached Ed25519 signature against `pk`. Used for standalone
806    /// node-identity bundles (QR codes, broadcasts) whose signed range must
807    /// verify before the bundle's claims are presented as authenticated.
808    pub fn verify_ed25519_signature(pk: &PublicKey, message: &[u8], signature: &[u8; 64]) -> bool {
809        let Ok(key) = ed25519_dalek::VerifyingKey::from_bytes(&pk.0) else {
810            return false;
811        };
812        key.verify_strict(message, &ed25519_dalek::Signature::from_bytes(signature))
813            .is_ok()
814    }
815}
816
817#[cfg(feature = "software-crypto")]
818pub use software::*;
819
820#[cfg(test)]
821mod tests {
822    use crate::{constant_time_eq, dbl};
823
824    #[cfg(feature = "software-crypto")]
825    use super::{software::*, *};
826    #[cfg(feature = "software-crypto")]
827    use umsh_core::{MicSize, NodeHint, PacketBuilder, PacketHeader, PublicKey};
828
829    #[cfg(feature = "software-crypto")]
830    #[test]
831    fn pairwise_hkdf_is_stable() {
832        let engine = SoftwareCryptoEngine::new(SoftwareAes, SoftwareSha256);
833        let keys = engine.derive_pairwise_keys(&SharedSecret([7u8; 32]));
834        assert_ne!(keys.k_enc, [0u8; 16]);
835        assert_ne!(keys.k_mic, [0u8; 16]);
836    }
837
838    #[cfg(feature = "software-crypto")]
839    #[test]
840    fn aes_cmac_matches_rfc4493_example_2() {
841        let engine = SoftwareCryptoEngine::new(SoftwareAes, SoftwareSha256);
842        let key = hex_16("2b7e151628aed2a6abf7158809cf4f3c");
843        let msg = hex_vec("6bc1bee22e409f96e93d7e117393172a");
844        let expected = hex_16("070a16b46b4d4144f79bdd9dd04a287c");
845        assert_eq!(engine.aes_cmac(&key, &[&msg]), expected);
846    }
847
848    #[cfg(feature = "software-crypto")]
849    #[test]
850    fn aes_cmac_matches_rfc4493_example_1_empty() {
851        let engine = SoftwareCryptoEngine::new(SoftwareAes, SoftwareSha256);
852        let key = hex_16("2b7e151628aed2a6abf7158809cf4f3c");
853        let expected = hex_16("bb1d6929e95937287fa37d129b756746");
854        assert_eq!(engine.aes_cmac(&key, &[&[]]), expected);
855    }
856
857    #[cfg(feature = "software-crypto")]
858    #[test]
859    fn aes_cmac_matches_rfc4493_example_3_64b() {
860        let engine = SoftwareCryptoEngine::new(SoftwareAes, SoftwareSha256);
861        let key = hex_16("2b7e151628aed2a6abf7158809cf4f3c");
862        let msg = hex_vec(
863            "6bc1bee22e409f96e93d7e117393172a\
864             ae2d8a571e03ac9c9eb76fac45af8e51\
865             30c81c46a35ce411e5fbc1191a0a52ef\
866             f69f2445df4f9b17ad2b417be66c3710",
867        );
868        let expected = hex_16("51f0bebf7e3b9d92fc49741779363cfe");
869        assert_eq!(engine.aes_cmac(&key, &[&msg]), expected);
870    }
871
872    #[cfg(feature = "software-crypto")]
873    #[test]
874    fn aes_cmac_matches_rfc4493_example_4_40b() {
875        let engine = SoftwareCryptoEngine::new(SoftwareAes, SoftwareSha256);
876        let key = hex_16("2b7e151628aed2a6abf7158809cf4f3c");
877        let msg = hex_vec(
878            "6bc1bee22e409f96e93d7e117393172a\
879             ae2d8a571e03ac9c9eb76fac45af8e51\
880             30c81c46a35ce411",
881        );
882        let expected = hex_16("dfa66747de9ae63030ca32611497c827");
883        assert_eq!(engine.aes_cmac(&key, &[&msg]), expected);
884    }
885
886    /// NIST SP 800-38A Section F.5.1 — AES-128 CTR mode, single block.
887    #[cfg(feature = "software-crypto")]
888    #[test]
889    fn aes_ctr_matches_nist_sp800_38a_block_1() {
890        let engine = SoftwareCryptoEngine::new(SoftwareAes, SoftwareSha256);
891        let key = hex_16("2b7e151628aed2a6abf7158809cf4f3c");
892        let iv = hex_16("f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff");
893        let mut data = hex_vec("6bc1bee22e409f96e93d7e117393172a");
894        engine.aes_ctr(&key, &iv, &mut data);
895        assert_eq!(data, hex_vec("874d6191b620e3261bef6864990db6ce"));
896    }
897
898    /// NIST SP 800-38A Section F.5.1 — AES-128 CTR mode, 4 blocks.
899    /// Verifies counter increment across multiple blocks.
900    #[cfg(feature = "software-crypto")]
901    #[test]
902    fn aes_ctr_matches_nist_sp800_38a_4_blocks() {
903        let engine = SoftwareCryptoEngine::new(SoftwareAes, SoftwareSha256);
904        let key = hex_16("2b7e151628aed2a6abf7158809cf4f3c");
905        let iv = hex_16("f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff");
906        let mut data = hex_vec(
907            "6bc1bee22e409f96e93d7e117393172a\
908             ae2d8a571e03ac9c9eb76fac45af8e51\
909             30c81c46a35ce411e5fbc1191a0a52ef\
910             f69f2445df4f9b17ad2b417be66c3710",
911        );
912        let expected = hex_vec(
913            "874d6191b620e3261bef6864990db6ce\
914             9806f66b7970fdff8617187bb9fffdff\
915             5ae4df3edbd5d35e5b4f09020db03eab\
916             1e031dda2fbe03d1792170a0f3009cee",
917        );
918        engine.aes_ctr(&key, &iv, &mut data);
919        assert_eq!(data, expected);
920    }
921
922    /// NIST SP 800-38A — CTR decrypt (symmetric operation).
923    #[cfg(feature = "software-crypto")]
924    #[test]
925    fn aes_ctr_decrypt_matches_nist_sp800_38a() {
926        let engine = SoftwareCryptoEngine::new(SoftwareAes, SoftwareSha256);
927        let key = hex_16("2b7e151628aed2a6abf7158809cf4f3c");
928        let iv = hex_16("f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff");
929        let mut data = hex_vec(
930            "874d6191b620e3261bef6864990db6ce\
931             9806f66b7970fdff8617187bb9fffdff\
932             5ae4df3edbd5d35e5b4f09020db03eab\
933             1e031dda2fbe03d1792170a0f3009cee",
934        );
935        let expected = hex_vec(
936            "6bc1bee22e409f96e93d7e117393172a\
937             ae2d8a571e03ac9c9eb76fac45af8e51\
938             30c81c46a35ce411e5fbc1191a0a52ef\
939             f69f2445df4f9b17ad2b417be66c3710",
940        );
941        engine.aes_ctr(&key, &iv, &mut data);
942        assert_eq!(data, expected);
943    }
944
945    /// AES-CTR with partial final block (non-aligned length).
946    #[cfg(feature = "software-crypto")]
947    #[test]
948    fn aes_ctr_partial_block() {
949        let engine = SoftwareCryptoEngine::new(SoftwareAes, SoftwareSha256);
950        let key = hex_16("2b7e151628aed2a6abf7158809cf4f3c");
951        let iv = hex_16("f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff");
952        // Encrypt 5 bytes (less than one block)
953        let mut data = hex_vec("6bc1bee22e");
954        engine.aes_ctr(&key, &iv, &mut data);
955        // Should match first 5 bytes of full block 1 ciphertext
956        assert_eq!(data, hex_vec("874d6191b6"));
957        // Decrypt back
958        engine.aes_ctr(&key, &iv, &mut data);
959        assert_eq!(data, hex_vec("6bc1bee22e"));
960    }
961
962    #[cfg(feature = "software-crypto")]
963    #[test]
964    fn hmac_sha256_matches_rfc4231_case_1() {
965        let sha = SoftwareSha256;
966        let key = [0x0bu8; 20];
967        let expected = hex_32("b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7");
968        assert_eq!(sha.hmac(&key, &[b"Hi There"]), expected);
969    }
970
971    #[cfg(feature = "software-crypto")]
972    #[test]
973    fn hkdf_matches_rfc5869_case_1() {
974        let engine = SoftwareCryptoEngine::new(SoftwareAes, SoftwareSha256);
975        let ikm = [0x0bu8; 22];
976        let salt = hex_vec("000102030405060708090a0b0c");
977        let info = hex_vec("f0f1f2f3f4f5f6f7f8f9");
978        let mut okm = [0u8; 42];
979        engine.hkdf(&ikm, &salt, &info, &mut okm);
980        assert_eq!(
981            okm.to_vec(),
982            hex_vec(
983                "3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865"
984            )
985        );
986    }
987
988    #[test]
989    fn cmac_doubling_matches_rfc4493_subkey_generation() {
990        let l = hex_16("7df76b0c1ab899b33e42f047b91b546f");
991        let expected_k1 = hex_16("fbeed618357133667c85e08f7236a8de");
992        let expected_k2 = hex_16("f7ddac306ae266ccf90bc11ee46d513b");
993        let k1 = dbl(&l);
994        let k2 = dbl(&k1);
995        assert_eq!(k1, expected_k1);
996        assert_eq!(k2, expected_k2);
997    }
998
999    #[test]
1000    fn constant_time_eq_rejects_mismatch() {
1001        assert!(constant_time_eq(&[1, 2, 3], &[1, 2, 3]));
1002        assert!(!constant_time_eq(&[1, 2, 3], &[1, 2, 4]));
1003        assert!(!constant_time_eq(&[1, 2, 3], &[1, 2, 3, 4]));
1004    }
1005
1006    #[cfg(feature = "software-crypto")]
1007    #[test]
1008    fn unicast_seal_and_open_round_trip() {
1009        let engine = SoftwareCryptoEngine::new(SoftwareAes, SoftwareSha256);
1010        let keys = engine.derive_pairwise_keys(&SharedSecret([9u8; 32]));
1011        let src = PublicKey([0xA1; 32]);
1012        let dst = NodeHint([0xC3, 0xD4, 0x25]);
1013        let mut buf = [0u8; 128];
1014        let mut packet = PacketBuilder::new(&mut buf)
1015            .unicast(dst)
1016            .source_full(&src)
1017            .frame_counter(1)
1018            .encrypted()
1019            .mic_size(MicSize::Mic16)
1020            .payload(b"hello")
1021            .build()
1022            .unwrap();
1023
1024        engine.seal_packet(&mut packet, &keys).unwrap();
1025        let header = PacketHeader::parse(packet.as_bytes()).unwrap();
1026        let mut wire = packet.as_bytes().to_vec();
1027        let range = engine.open_packet(&mut wire, &header, &keys).unwrap();
1028        assert_eq!(&wire[range], b"hello");
1029    }
1030
1031    /// Regression: with a MIC shorter than 16 bytes the CTR IV includes
1032    /// SECINFO bytes, and SECINFO precedes the options block — not the
1033    /// body, which the options end marker separates from it. The IV of
1034    /// a received packet must be built from the true SECINFO position
1035    /// or decryption diverges from sealing.
1036    #[cfg(feature = "software-crypto")]
1037    #[test]
1038    fn short_mic_encrypted_unicast_round_trip() {
1039        let engine = SoftwareCryptoEngine::new(SoftwareAes, SoftwareSha256);
1040        let keys = engine.derive_pairwise_keys(&SharedSecret([9u8; 32]));
1041        let mut buf = [0u8; 128];
1042        let mut packet = PacketBuilder::new(&mut buf)
1043            .unicast(NodeHint([0xC3, 0xD4, 0x25]))
1044            .source_hint(NodeHint([0xA1, 0xA1, 0xA1]))
1045            .frame_counter(7)
1046            .encrypted()
1047            .mic_size(MicSize::Mic8)
1048            .payload(b"short mic")
1049            .build()
1050            .unwrap();
1051        engine.seal_packet(&mut packet, &keys).unwrap();
1052        let header = PacketHeader::parse(packet.as_bytes()).unwrap();
1053        let mut wire = packet.as_bytes().to_vec();
1054        let range = engine.open_packet(&mut wire, &header, &keys).unwrap();
1055        assert_eq!(&wire[range], b"short mic");
1056    }
1057
1058    /// Regression companion to the above for blind unicast, where the
1059    /// concealed address block also sits between SECINFO and the body:
1060    /// address decryption and payload opening must both locate SECINFO
1061    /// correctly with a short MIC.
1062    #[cfg(feature = "software-crypto")]
1063    #[test]
1064    fn short_mic_blind_unicast_round_trip() {
1065        let engine = SoftwareCryptoEngine::new(SoftwareAes, SoftwareSha256);
1066        let channel_key = ChannelKey([0x5A; 32]);
1067        let channel_keys = engine.derive_channel_keys(&channel_key);
1068        let pairwise = engine.derive_pairwise_keys(&SharedSecret([9u8; 32]));
1069        let blind_keys = engine.derive_blind_keys(&pairwise, &channel_keys);
1070        let dst = NodeHint([0xC3, 0xD4, 0x25]);
1071        let src = NodeHint([0xA1, 0xA1, 0xA1]);
1072
1073        let mut buf = [0u8; 128];
1074        let mut packet = PacketBuilder::new(&mut buf)
1075            .blind_unicast(channel_keys.channel_id, dst)
1076            .source_hint(src)
1077            .frame_counter(7)
1078            .ack_requested()
1079            .encrypted()
1080            .mic_size(MicSize::Mic8)
1081            .payload(b"blind")
1082            .build()
1083            .unwrap();
1084        engine
1085            .seal_blind_packet(&mut packet, &blind_keys, &channel_keys)
1086            .unwrap();
1087
1088        let header = PacketHeader::parse(packet.as_bytes()).unwrap();
1089        let mut wire = packet.as_bytes().to_vec();
1090        let (decrypted_dst, decrypted_src) = engine
1091            .decrypt_blind_addr(&mut wire, &header, &channel_keys)
1092            .unwrap();
1093        assert_eq!(decrypted_dst, dst);
1094        assert_eq!(decrypted_src, SourceAddrRef::Hint(src));
1095        let range = engine.open_packet(&mut wire, &header, &blind_keys).unwrap();
1096        assert_eq!(&wire[range], b"blind");
1097    }
1098
1099    /// Verify compute_ack_trailer produces a deterministic 8-byte value and
1100    /// that its two halves are the public `ack_mic` (CMAC prefix) and the
1101    /// keyed `ack_tag`.
1102    #[cfg(feature = "software-crypto")]
1103    #[test]
1104    fn compute_ack_trailer_is_deterministic() {
1105        let engine = SoftwareCryptoEngine::new(SoftwareAes, SoftwareSha256);
1106        let key = hex_16("2b7e151628aed2a6abf7158809cf4f3c");
1107        let cmac = hex_16("070a16b46b4d4144f79bdd9dd04a287c"); // RFC 4493 example 2
1108        let t1 = engine.compute_ack_trailer(&cmac, &key);
1109        let t2 = engine.compute_ack_trailer(&cmac, &key);
1110        assert_eq!(t1, t2);
1111        assert_eq!(t1.len(), 8);
1112        // ack_mic half is the first 4 bytes of the full CMAC (public).
1113        assert_eq!(t1[..4], cmac[..4]);
1114        // ack_tag half is the first 4 bytes of AES-ECB(key, cmac) (keyed).
1115        let cipher = SoftwareAes.new_cipher(&key);
1116        let mut block = cmac;
1117        cipher.encrypt_block(&mut block);
1118        assert_eq!(t1[4..], block[..4]);
1119    }
1120
1121    /// Verify that compute_ack_trailer with different CMACs produces different
1122    /// trailers in both the mic and tag halves.
1123    #[cfg(feature = "software-crypto")]
1124    #[test]
1125    fn compute_ack_trailer_differs_for_different_cmacs() {
1126        let engine = SoftwareCryptoEngine::new(SoftwareAes, SoftwareSha256);
1127        let key = hex_16("2b7e151628aed2a6abf7158809cf4f3c");
1128        let cmac_a = hex_16("070a16b46b4d4144f79bdd9dd04a287c");
1129        let cmac_b = hex_16("51f0bebf7e3b9d92fc49741779363cfe");
1130        let t_a = engine.compute_ack_trailer(&cmac_a, &key);
1131        let t_b = engine.compute_ack_trailer(&cmac_b, &key);
1132        assert_ne!(t_a, t_b);
1133        assert_ne!(t_a, [0u8; 8]);
1134        assert_ne!(t_b, [0u8; 8]);
1135    }
1136
1137    #[cfg(feature = "software-crypto")]
1138    #[test]
1139    fn encrypted_multicast_round_trip_preserves_source_prefix() {
1140        let engine = SoftwareCryptoEngine::new(SoftwareAes, SoftwareSha256);
1141        let channel_key = ChannelKey([0x5Au8; 32]);
1142        let derived = engine.derive_channel_keys(&channel_key);
1143        let src = PublicKey([0xA1; 32]);
1144        let mut buf = [0u8; 160];
1145        let mut packet = PacketBuilder::new(&mut buf)
1146            .multicast(derived.channel_id)
1147            .source_hint(src.hint())
1148            .frame_counter(5)
1149            .encrypted()
1150            .mic_size(MicSize::Mic16)
1151            .payload(b"hello")
1152            .build()
1153            .unwrap();
1154
1155        let multicast_keys = PairwiseKeys {
1156            k_enc: derived.k_enc,
1157            k_mic: derived.k_mic,
1158        };
1159        engine.seal_packet(&mut packet, &multicast_keys).unwrap();
1160        let header = PacketHeader::parse(packet.as_bytes()).unwrap();
1161        let mut wire = packet.as_bytes().to_vec();
1162        let range = engine
1163            .open_packet(&mut wire, &header, &multicast_keys)
1164            .unwrap();
1165        assert_eq!(
1166            &wire[header.body_range.start..header.body_range.start + 3],
1167            &src.hint().0
1168        );
1169        assert_eq!(&wire[range], b"hello");
1170    }
1171
1172    #[cfg(feature = "software-crypto")]
1173    #[test]
1174    fn blind_unicast_round_trip_recovers_addresses_and_payload() {
1175        let engine = SoftwareCryptoEngine::new(SoftwareAes, SoftwareSha256);
1176        let shared = SharedSecret([0x33u8; 32]);
1177        let pairwise = engine.derive_pairwise_keys(&shared);
1178        let channel_key = ChannelKey([0x5Au8; 32]);
1179        let channel = engine.derive_channel_keys(&channel_key);
1180        let blind_keys = engine.derive_blind_keys(&pairwise, &channel);
1181        let src = PublicKey([
1182            0xA1, 0xB2, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E,
1183            0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C,
1184            0x1D, 0x1E, 0x1F, 0x20,
1185        ]);
1186        let dst = NodeHint([0xC3, 0xD4, 0x25]);
1187        let mut buf = [0u8; 160];
1188        let mut packet = PacketBuilder::new(&mut buf)
1189            .blind_unicast(channel.channel_id, dst)
1190            .source_full(&src)
1191            .frame_counter(5)
1192            .mic_size(MicSize::Mic16)
1193            .payload(b"hello")
1194            .build()
1195            .unwrap();
1196
1197        engine
1198            .seal_blind_packet(&mut packet, &blind_keys, &channel)
1199            .unwrap();
1200        let header = PacketHeader::parse(packet.as_bytes()).unwrap();
1201        let mut wire = packet.as_bytes().to_vec();
1202        let (decoded_dst, decoded_src) = engine
1203            .decrypt_blind_addr(&mut wire, &header, &channel)
1204            .unwrap();
1205        let range = engine.open_packet(&mut wire, &header, &blind_keys).unwrap();
1206
1207        assert_eq!(decoded_dst, dst);
1208        assert_eq!(
1209            decoded_src,
1210            SourceAddrRef::FullKeyAt {
1211                offset: header.body_range.start - 32
1212            }
1213        );
1214        assert_eq!(
1215            &wire[header.body_range.start - 32..header.body_range.start],
1216            &src.0
1217        );
1218        assert_eq!(&wire[range], b"hello");
1219    }
1220
1221    #[cfg(feature = "software-crypto")]
1222    #[test]
1223    fn software_identity_agreement_is_symmetric() {
1224        let alice = SoftwareIdentity::from_secret_bytes(&[
1225            0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E,
1226            0x1F, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x2B, 0x2C,
1227            0x2D, 0x2E, 0x2F, 0x30,
1228        ]);
1229        let bob = SoftwareIdentity::from_secret_bytes(&[
1230            0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E,
1231            0x3F, 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x4B, 0x4C,
1232            0x4D, 0x4E, 0x4F, 0x50,
1233        ]);
1234
1235        let ab = alice.shared_secret_with(bob.public_key()).unwrap();
1236        let ba = bob.shared_secret_with(alice.public_key()).unwrap();
1237        assert_eq!(ab.0, ba.0);
1238    }
1239
1240    #[cfg(feature = "software-crypto")]
1241    #[test]
1242    fn channel_tint_extends_the_channel_identifier() {
1243        let engine = SoftwareCryptoEngine::new(SoftwareAes, SoftwareSha256);
1244        let key = ChannelKey([0x5Au8; 32]);
1245        let tint = engine.derive_channel_tint(&key);
1246        // The presentation colour is the identifier derivation run one byte
1247        // longer, so a caller holding either can recognize the other.
1248        assert_eq!(&tint[..2], &engine.derive_channel_id(&key).0[..]);
1249        assert_eq!(tint, engine.derive_channel_tint(&key));
1250        assert_ne!(tint, engine.derive_channel_tint(&ChannelKey([0x5Bu8; 32])));
1251    }
1252
1253    #[cfg(feature = "software-crypto")]
1254    #[test]
1255    fn channel_tag_extends_the_channel_identifier_and_tint() {
1256        let engine = SoftwareCryptoEngine::new(SoftwareAes, SoftwareSha256);
1257        let key = ChannelKey([0x5Au8; 32]);
1258        let tag = engine.derive_channel_tag(&key);
1259        // One derivation, three lengths: whoever holds the tag can recover the
1260        // identifier and the tint without the key.
1261        assert_eq!(tag.channel_id(), engine.derive_channel_id(&key));
1262        assert_eq!(&tag.0[..3], &engine.derive_channel_tint(&key)[..]);
1263        assert_eq!(tag, engine.derive_channel_tag(&key));
1264        assert_ne!(tag, engine.derive_channel_tag(&ChannelKey([0x5Bu8; 32])));
1265    }
1266
1267    // ── derive_named_channel_key ──────────────────────────────────────────────
1268
1269    #[cfg(feature = "software-crypto")]
1270    #[test]
1271    fn derive_named_channel_key_is_deterministic() {
1272        let engine = SoftwareCryptoEngine::new(SoftwareAes, SoftwareSha256);
1273        let k1 = engine.derive_named_channel_key("lobby").unwrap();
1274        let k2 = engine.derive_named_channel_key("lobby").unwrap();
1275        let k3 = engine.derive_named_channel_key("other").unwrap();
1276        assert_eq!(k1, k2);
1277        assert_ne!(k1, k3);
1278        assert_ne!(k1, ChannelKey([0u8; 32]));
1279    }
1280
1281    #[cfg(feature = "software-crypto")]
1282    #[test]
1283    fn derive_named_channel_key_folds_ascii_case() {
1284        let engine = SoftwareCryptoEngine::new(SoftwareAes, SoftwareSha256);
1285        let lower = engine.derive_named_channel_key("public").unwrap();
1286        assert_eq!(engine.derive_named_channel_key("Public").unwrap(), lower);
1287        assert_eq!(engine.derive_named_channel_key("PUBLIC").unwrap(), lower);
1288        // Digits and symbols are untouched by the fold.
1289        assert_eq!(
1290            engine.derive_named_channel_key("Mesh-42!").unwrap(),
1291            engine.derive_named_channel_key("mesh-42!").unwrap()
1292        );
1293    }
1294
1295    #[cfg(feature = "software-crypto")]
1296    #[test]
1297    fn derive_named_channel_key_rejects_invalid_names() {
1298        let engine = SoftwareCryptoEngine::new(SoftwareAes, SoftwareSha256);
1299        assert_eq!(
1300            engine.derive_named_channel_key("caf\u{e9}"),
1301            Err(ChannelNameError::NotAscii)
1302        );
1303        let too_long = "x".repeat(MAX_CHANNEL_NAME_LEN + 1);
1304        assert_eq!(
1305            engine.derive_named_channel_key(&too_long),
1306            Err(ChannelNameError::TooLong)
1307        );
1308        // Exactly at the limit is fine.
1309        let at_limit = "x".repeat(MAX_CHANNEL_NAME_LEN);
1310        assert!(engine.derive_named_channel_key(&at_limit).is_ok());
1311    }
1312
1313    // ── SoftwareIdentity::generate ────────────────────────────────────────────
1314
1315    #[cfg(feature = "software-crypto")]
1316    #[test]
1317    fn software_identity_generate_produces_valid_key() {
1318        use rand_core::{CryptoRng, RngCore};
1319
1320        // Minimal deterministic RNG for testing — counter-based, not cryptographically
1321        // strong, but sufficient to exercise the generate() code path.
1322        struct CounterRng(u64);
1323        impl RngCore for CounterRng {
1324            fn next_u32(&mut self) -> u32 {
1325                self.next_u64() as u32
1326            }
1327            fn next_u64(&mut self) -> u64 {
1328                self.0 = self.0.wrapping_add(1);
1329                self.0
1330            }
1331            fn fill_bytes(&mut self, dest: &mut [u8]) {
1332                for chunk in dest.chunks_mut(8) {
1333                    let bytes = self.next_u64().to_le_bytes();
1334                    chunk.copy_from_slice(&bytes[..chunk.len()]);
1335                }
1336            }
1337            fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), rand_core::Error> {
1338                self.fill_bytes(dest);
1339                Ok(())
1340            }
1341        }
1342        impl CryptoRng for CounterRng {}
1343
1344        let mut rng1 = CounterRng(0);
1345        let mut rng2 = CounterRng(1000);
1346        let id1 = SoftwareIdentity::generate(&mut rng1);
1347        let id2 = SoftwareIdentity::generate(&mut rng2);
1348        assert_ne!(id1.public_key(), id2.public_key());
1349        let ss1 = id1.shared_secret_with(id2.public_key()).unwrap();
1350        let ss2 = id2.shared_secret_with(id1.public_key()).unwrap();
1351        assert_eq!(ss1.0, ss2.0);
1352    }
1353
1354    // ── seal_blind_packet wrong packet type ───────────────────────────────────
1355
1356    #[cfg(feature = "software-crypto")]
1357    #[test]
1358    fn seal_blind_packet_rejects_non_blind_packet() {
1359        let engine = SoftwareCryptoEngine::new(SoftwareAes, SoftwareSha256);
1360        let shared = SharedSecret([0x11u8; 32]);
1361        let pairwise = engine.derive_pairwise_keys(&shared);
1362        let channel_key = ChannelKey([0x5Au8; 32]);
1363        let channel = engine.derive_channel_keys(&channel_key);
1364        let blind_keys = engine.derive_blind_keys(&pairwise, &channel);
1365        let src = PublicKey([0xA1u8; 32]);
1366        let dst = NodeHint([0xC3, 0xD4, 0x25]);
1367        let mut buf = [0u8; 128];
1368        // Build a regular unicast packet (not a blind unicast).
1369        let mut packet = PacketBuilder::new(&mut buf)
1370            .unicast(dst)
1371            .source_full(&src)
1372            .frame_counter(1)
1373            .encrypted()
1374            .mic_size(MicSize::Mic16)
1375            .payload(b"test")
1376            .build()
1377            .unwrap();
1378        assert!(matches!(
1379            engine.seal_blind_packet(&mut packet, &blind_keys, &channel),
1380            Err(CryptoError::InvalidPacket)
1381        ));
1382    }
1383
1384    // ── open_packet authentication failure ────────────────────────────────────
1385
1386    #[cfg(feature = "software-crypto")]
1387    #[test]
1388    fn open_packet_fails_on_tampered_ciphertext() {
1389        let engine = SoftwareCryptoEngine::new(SoftwareAes, SoftwareSha256);
1390        let keys = engine.derive_pairwise_keys(&SharedSecret([9u8; 32]));
1391        let src = PublicKey([0xA1; 32]);
1392        let dst = NodeHint([0xC3, 0xD4, 0x25]);
1393        let mut buf = [0u8; 128];
1394        let mut packet = PacketBuilder::new(&mut buf)
1395            .unicast(dst)
1396            .source_full(&src)
1397            .frame_counter(1)
1398            .encrypted()
1399            .mic_size(MicSize::Mic16)
1400            .payload(b"hello")
1401            .build()
1402            .unwrap();
1403        engine.seal_packet(&mut packet, &keys).unwrap();
1404
1405        // Flip a bit in the sealed packet body.
1406        let mut wire = packet.as_bytes().to_vec();
1407        let header = PacketHeader::parse(&wire).unwrap();
1408        wire[header.body_range.start] ^= 0x01;
1409
1410        assert!(matches!(
1411            engine.open_packet(&mut wire, &header, &keys),
1412            Err(CryptoError::AuthenticationFailed)
1413        ));
1414    }
1415
1416    // ── decrypt_blind_addr: encrypted hint source (len == 3) ─────────────────
1417
1418    #[cfg(feature = "software-crypto")]
1419    #[test]
1420    fn decrypt_blind_addr_encrypted_hint_source() {
1421        let engine = SoftwareCryptoEngine::new(SoftwareAes, SoftwareSha256);
1422        let shared = SharedSecret([0x33u8; 32]);
1423        let pairwise = engine.derive_pairwise_keys(&shared);
1424        let channel_key = ChannelKey([0x5Au8; 32]);
1425        let channel = engine.derive_channel_keys(&channel_key);
1426        let blind_keys = engine.derive_blind_keys(&pairwise, &channel);
1427        let src = PublicKey([0xA1u8; 32]);
1428        let dst = NodeHint([0xC3, 0xD4, 0x25]);
1429        let mut buf = [0u8; 128];
1430        // Use source_hint → produces Encrypted { len: 3 } in blind unicast.
1431        let mut packet = PacketBuilder::new(&mut buf)
1432            .blind_unicast(channel.channel_id, dst)
1433            .source_hint(src.hint())
1434            .frame_counter(5)
1435            .encrypted()
1436            .mic_size(MicSize::Mic16)
1437            .payload(b"hi")
1438            .build()
1439            .unwrap();
1440        engine
1441            .seal_blind_packet(&mut packet, &blind_keys, &channel)
1442            .unwrap();
1443        let mut wire = packet.as_bytes().to_vec();
1444        let header = PacketHeader::parse(&wire).unwrap();
1445        let (decoded_dst, decoded_src) = engine
1446            .decrypt_blind_addr(&mut wire, &header, &channel)
1447            .unwrap();
1448        assert_eq!(decoded_dst, dst);
1449        assert_eq!(decoded_src, SourceAddrRef::Hint(src.hint()));
1450    }
1451
1452    // ── SoftwareAes::decrypt_block ────────────────────────────────────────────
1453
1454    #[cfg(feature = "software-crypto")]
1455    #[test]
1456    fn aes_decrypt_block_inverts_encrypt_block() {
1457        let key = hex_16("2b7e151628aed2a6abf7158809cf4f3c");
1458        let cipher = SoftwareAes.new_cipher(&key);
1459        let original = hex_16("6bc1bee22e409f96e93d7e117393172a");
1460        let mut block = original;
1461        cipher.encrypt_block(&mut block);
1462        assert_ne!(block, original);
1463        cipher.decrypt_block(&mut block);
1464        assert_eq!(block, original);
1465    }
1466
1467    // ── NodeIdentity trait: sign and agree ────────────────────────────────────
1468
1469    #[cfg(feature = "software-crypto")]
1470    #[test]
1471    fn sign_and_agree_via_trait() {
1472        use core::future::Future;
1473        use core::pin::pin;
1474        use core::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};
1475
1476        fn block_on<F: Future>(future: F) -> F::Output {
1477            let raw = RawWaker::new(
1478                core::ptr::null(),
1479                &RawWakerVTable::new(|_| panic!(), |_| {}, |_| {}, |_| {}),
1480            );
1481            let waker = unsafe { Waker::from_raw(raw) };
1482            let mut ctx = Context::from_waker(&waker);
1483            match pin!(future).poll(&mut ctx) {
1484                Poll::Ready(v) => v,
1485                Poll::Pending => panic!("future unexpectedly pending"),
1486            }
1487        }
1488
1489        let alice = SoftwareIdentity::from_secret_bytes(&[0x11u8; 32]);
1490        let bob = SoftwareIdentity::from_secret_bytes(&[0x22u8; 32]);
1491
1492        let sig = block_on(alice.sign(b"test message")).unwrap();
1493        assert_eq!(sig.len(), 64);
1494
1495        let shared = block_on(alice.agree(bob.public_key())).unwrap();
1496        assert_ne!(shared.0, [0u8; 32]);
1497    }
1498
1499    // ── NodeIdentity::hint default impl ───────────────────────────────────────
1500
1501    #[cfg(feature = "software-crypto")]
1502    #[test]
1503    fn node_identity_hint_matches_public_key_hint() {
1504        let id = SoftwareIdentity::from_secret_bytes(&[0x11u8; 32]);
1505        // Calls the NodeIdentity::hint() default impl, which derives from public_key().
1506        assert_eq!(NodeIdentity::hint(&id), id.public_key().hint());
1507    }
1508
1509    // ── uppercase hex in decode_hex ───────────────────────────────────────────
1510
1511    #[test]
1512    fn hex_vec_accepts_uppercase() {
1513        assert_eq!(hex_vec("DEADBEEF"), hex_vec("deadbeef"));
1514        assert_eq!(hex_vec("0A1B2C3D"), hex_vec("0a1b2c3d"));
1515    }
1516
1517    fn hex_vec(input: &str) -> std::vec::Vec<u8> {
1518        assert_eq!(input.len() % 2, 0);
1519        let mut out = std::vec::Vec::with_capacity(input.len() / 2);
1520        let bytes = input.as_bytes();
1521        for index in (0..bytes.len()).step_by(2) {
1522            out.push((decode_hex(bytes[index]) << 4) | decode_hex(bytes[index + 1]));
1523        }
1524        out
1525    }
1526
1527    fn hex_16(input: &str) -> [u8; 16] {
1528        let bytes = hex_vec(input);
1529        let mut out = [0u8; 16];
1530        out.copy_from_slice(&bytes);
1531        out
1532    }
1533
1534    #[cfg(feature = "software-crypto")]
1535    fn hex_32(input: &str) -> [u8; 32] {
1536        let bytes = hex_vec(input);
1537        let mut out = [0u8; 32];
1538        out.copy_from_slice(&bytes);
1539        out
1540    }
1541
1542    fn decode_hex(byte: u8) -> u8 {
1543        match byte {
1544            b'0'..=b'9' => byte - b'0',
1545            b'a'..=b'f' => byte - b'a' + 10,
1546            b'A'..=b'F' => byte - b'A' + 10,
1547            _ => panic!("invalid hex"),
1548        }
1549    }
1550
1551    #[cfg(feature = "software-crypto")]
1552    #[test]
1553    fn is_valid_ed25519_public_key_accepts_real_key() {
1554        // Any verifying key produced by a SigningKey is by construction a
1555        // valid Ed25519 point — the validator must accept it.
1556        let identity = SoftwareIdentity::from_secret_bytes(&[0x11; 32]);
1557        assert!(is_valid_ed25519_public_key(identity.public_key()));
1558    }
1559
1560    #[cfg(feature = "software-crypto")]
1561    #[test]
1562    fn is_valid_ed25519_public_key_rejects_non_curve_bytes() {
1563        // Y = 2 (little-endian) is not on the Ed25519 curve: the
1564        // recovered x^2 from the curve equation has no square root in
1565        // GF(2^255 - 19). Confirm the validator rejects it — this is
1566        // the exact failure mode a user typo trips when they paste a
1567        // mistyped hex pubkey on the CLI.
1568        let mut bytes = [0u8; 32];
1569        bytes[0] = 2;
1570        let bogus = PublicKey(bytes);
1571        assert!(!is_valid_ed25519_public_key(&bogus));
1572    }
1573
1574    #[cfg(feature = "software-crypto")]
1575    #[test]
1576    fn is_valid_ed25519_public_key_handles_all_zero() {
1577        // The all-zero compressed Y value decompresses to the curve
1578        // identity point. Whatever the library's actual behavior is,
1579        // pin it so a curve25519-dalek update can't silently flip the
1580        // contract. (Today: accepted as a valid point.)
1581        let zero = PublicKey([0u8; 32]);
1582        assert!(is_valid_ed25519_public_key(&zero));
1583    }
1584}