Skip to main content

libsodium_rs/
crypto_ipcrypt.rs

1//! # IP Address Encryption
2//!
3//! This module provides efficient, secure encryption of IP addresses (IPv4 and IPv6)
4//! for privacy-preserving storage, logging, and analytics.
5//!
6//! Unlike truncation (which irreversibly destroys data) or hashing (which prevents
7//! decryption), ipcrypt provides reversible encryption with well-defined security
8//! properties while maintaining operational utility.
9//!
10//! ## Use Cases
11//!
12//! - **Privacy-preserving logs**: Encrypt IP addresses in web server logs while
13//!   retaining the ability to decrypt for abuse investigation
14//! - **Rate limiting and abuse detection**: Use deterministic mode to identify
15//!   repeat clients without storing plaintext IPs
16//! - **Analytics without exposure**: Count unique visitors without exposing actual
17//!   addresses to third-party analytics services
18//! - **Regulatory compliance**: Store IP addresses in encrypted form for GDPR/CCPA
19//!   compliance while maintaining lawful interception capability
20//!
21//! ## Variants
22//!
23//! | Variant | Key Size | Output Size | Properties |
24//! |---------|----------|-------------|------------|
25//! | Deterministic | 16 bytes | 16 bytes (IP string) | Same input always produces same output; format-preserving |
26//! | ND (non-deterministic) | 16 bytes | 24 bytes (hex) | Different output each time; 8-byte random tweak |
27//! | NDX (extended ND) | 32 bytes | 32 bytes (hex) | Different output each time; 16-byte random tweak |
28//! | PFX (prefix-preserving) | 32 bytes | 16 bytes (IP string) | Preserves network prefix relationships |
29//!
30//! ## Choosing the Right Variant
31//!
32//! - **Deterministic**: Rate limiting, deduplication, database indexing
33//! - **ND**: Log archival, third-party analytics, data exports (<4B encryptions/key)
34//! - **NDX**: Maximum security, billions of encryptions per key
35//! - **PFX**: Network analysis, DDoS research, PCAP anonymization
36//!
37//! ## IP Address Representation
38//!
39//! IP addresses can be provided as strings (both IPv4 and IPv6 are supported) or
40//! as 16-byte binary arrays:
41//! - IPv6: Used directly (16 bytes in network byte order)
42//! - IPv4: Encoded as IPv4-mapped IPv6 (`::ffff:a.b.c.d`)
43//!
44//! Use [`parse_ip`] to convert strings to binary, and [`format_ip`] for the reverse.
45//!
46//! ## Quick Start (String API)
47//!
48//! The simplest way to use this module is with the string-based functions:
49//!
50//! ```rust
51//! use libsodium_rs::crypto_ipcrypt;
52//!
53//! // Deterministic encryption (IP string in, IP string out)
54//! let key = crypto_ipcrypt::Key::generate();
55//! let encrypted = crypto_ipcrypt::encrypt_str("192.0.2.1", &key).unwrap();
56//! let decrypted = crypto_ipcrypt::decrypt_str(&encrypted, &key).unwrap();
57//! assert_eq!(decrypted, "192.0.2.1");
58//!
59//! // Works with IPv6 too
60//! let encrypted = crypto_ipcrypt::encrypt_str("2001:db8::1", &key).unwrap();
61//! let decrypted = crypto_ipcrypt::decrypt_str(&encrypted, &key).unwrap();
62//! assert_eq!(decrypted, "2001:db8::1");
63//! ```
64//!
65//! ## Non-Deterministic Encryption (String API)
66//!
67//! For ND and NDX variants, output is returned as hex since it exceeds 16 bytes:
68//!
69//! ```rust
70//! use libsodium_rs::crypto_ipcrypt;
71//!
72//! // ND: 24-byte output as hex (48 characters)
73//! let key = crypto_ipcrypt::Key::generate();
74//! let encrypted = crypto_ipcrypt::nd::encrypt_str("192.0.2.1", &key).unwrap();
75//! assert_eq!(encrypted.len(), 48);  // 24 bytes * 2 for hex
76//! let decrypted = crypto_ipcrypt::nd::decrypt_str(&encrypted, &key).unwrap();
77//! assert_eq!(decrypted, "192.0.2.1");
78//!
79//! // NDX: 32-byte output as hex (64 characters)
80//! let key = crypto_ipcrypt::ndx::Key::generate();
81//! let encrypted = crypto_ipcrypt::ndx::encrypt_str("192.0.2.1", &key).unwrap();
82//! assert_eq!(encrypted.len(), 64);  // 32 bytes * 2 for hex
83//! let decrypted = crypto_ipcrypt::ndx::decrypt_str(&encrypted, &key).unwrap();
84//! assert_eq!(decrypted, "192.0.2.1");
85//! ```
86//!
87//! ## Binary API
88//!
89//! For maximum control, use the binary API:
90//!
91//! ```rust
92//! use libsodium_rs::crypto_ipcrypt;
93//!
94//! let key = crypto_ipcrypt::Key::generate();
95//!
96//! // Parse IP string to binary
97//! let ip = crypto_ipcrypt::parse_ip("192.0.2.1").unwrap();
98//!
99//! // Or construct manually for IPv4
100//! let ip = crypto_ipcrypt::ipv4_to_bytes([192, 0, 2, 1]);
101//!
102//! // Encrypt/decrypt binary
103//! let encrypted = crypto_ipcrypt::encrypt(&ip, &key);
104//! let decrypted = crypto_ipcrypt::decrypt(&encrypted, &key);
105//! assert_eq!(ip, decrypted);
106//!
107//! // Format back to string
108//! let ip_str = crypto_ipcrypt::format_ip(&decrypted);
109//! assert_eq!(ip_str, "192.0.2.1");
110//! ```
111//!
112//! ## Hex Encoding for ND/NDX
113//!
114//! ND and NDX variants provide hex encoding helpers:
115//!
116//! ```rust
117//! use libsodium_rs::crypto_ipcrypt;
118//!
119//! let key = crypto_ipcrypt::Key::generate();
120//! let ip = crypto_ipcrypt::parse_ip("192.0.2.1").unwrap();
121//! let tweak = crypto_ipcrypt::nd::Tweak::random();
122//!
123//! // Encrypt to binary
124//! let encrypted = crypto_ipcrypt::nd::encrypt(&ip, &tweak, &key);
125//!
126//! // Convert to hex for storage/transmission
127//! let hex = crypto_ipcrypt::nd::to_hex(&encrypted);
128//!
129//! // Convert back from hex
130//! let recovered = crypto_ipcrypt::nd::from_hex(&hex).unwrap();
131//! assert_eq!(encrypted, recovered);
132//! ```
133
134use crate::random;
135use crate::SodiumError;
136
137/// Number of bytes in an IP address (16)
138pub const BYTES: usize = libsodium_sys::crypto_ipcrypt_BYTES as usize;
139
140/// Maximum length of an IP address string (45 chars + null terminator)
141/// IPv6 worst case: "ffff:ffff:ffff:ffff:ffff:ffff:255.255.255.255"
142const IP_MAXLEN: usize = 46;
143
144/// Number of bytes in a deterministic key (16)
145pub const KEYBYTES: usize = libsodium_sys::crypto_ipcrypt_KEYBYTES as usize;
146
147/// A key for deterministic IP address encryption
148///
149/// This key is used for the deterministic variant where the same input
150/// always produces the same output.
151#[derive(Clone, zeroize::Zeroize, zeroize::ZeroizeOnDrop)]
152pub struct Key([u8; KEYBYTES]);
153
154impl Key {
155    /// Generates a new random key
156    pub fn generate() -> Self {
157        let mut key = [0u8; KEYBYTES];
158        unsafe {
159            libsodium_sys::crypto_ipcrypt_keygen(key.as_mut_ptr());
160        }
161        Key(key)
162    }
163
164    /// Creates a key from a byte slice
165    ///
166    /// # Panics
167    ///
168    /// Panics if the slice is not exactly `KEYBYTES` bytes long
169    pub fn from_slice(slice: &[u8]) -> Self {
170        assert_eq!(slice.len(), KEYBYTES, "key must be {KEYBYTES} bytes");
171        let mut key = [0u8; KEYBYTES];
172        key.copy_from_slice(slice);
173        Key(key)
174    }
175
176    /// Returns the key as a byte slice
177    pub fn as_bytes(&self) -> &[u8] {
178        &self.0
179    }
180}
181
182impl AsRef<[u8]> for Key {
183    fn as_ref(&self) -> &[u8] {
184        &self.0
185    }
186}
187
188impl From<[u8; KEYBYTES]> for Key {
189    fn from(bytes: [u8; KEYBYTES]) -> Self {
190        Key(bytes)
191    }
192}
193
194/// Converts an IPv4 address to the 16-byte IPv4-mapped IPv6 representation
195///
196/// The result is in the format `::ffff:a.b.c.d`:
197/// 10 zero bytes + 0xff 0xff + 4 IPv4 bytes
198///
199/// # Example
200///
201/// ```rust
202/// use libsodium_rs::crypto_ipcrypt;
203///
204/// let ip = crypto_ipcrypt::ipv4_to_bytes([192, 0, 2, 1]);
205/// assert_eq!(ip[10], 0xff);
206/// assert_eq!(ip[11], 0xff);
207/// assert_eq!(&ip[12..], &[192, 0, 2, 1]);
208/// ```
209pub fn ipv4_to_bytes(addr: [u8; 4]) -> [u8; BYTES] {
210    let mut bytes = [0u8; BYTES];
211    bytes[10] = 0xff;
212    bytes[11] = 0xff;
213    bytes[12..].copy_from_slice(&addr);
214    bytes
215}
216
217/// Converts a 16-byte representation back to an IPv4 address if it's IPv4-mapped
218///
219/// Returns `Some([a, b, c, d])` if the input is an IPv4-mapped address,
220/// `None` otherwise.
221pub fn bytes_to_ipv4(bytes: &[u8; BYTES]) -> Option<[u8; 4]> {
222    // Check for IPv4-mapped prefix (10 zeros + 0xff 0xff)
223    if bytes[..10].iter().all(|&b| b == 0) && bytes[10] == 0xff && bytes[11] == 0xff {
224        let mut addr = [0u8; 4];
225        addr.copy_from_slice(&bytes[12..]);
226        Some(addr)
227    } else {
228        None
229    }
230}
231
232/// Parses an IP address string into the 16-byte binary representation
233///
234/// Accepts both IPv4 (e.g., "192.0.2.1") and IPv6 (e.g., "2001:db8::1") addresses.
235/// IPv4 addresses are automatically converted to IPv4-mapped format.
236/// IPv6 addresses with zone identifiers (e.g., "fe80::1%eth0") are also supported.
237///
238/// This uses libsodium's `sodium_ip2bin()` function internally.
239///
240/// # Example
241///
242/// ```rust
243/// use libsodium_rs::crypto_ipcrypt;
244///
245/// // Parse IPv4
246/// let ipv4 = crypto_ipcrypt::parse_ip("192.0.2.1").unwrap();
247///
248/// // Parse IPv6
249/// let ipv6 = crypto_ipcrypt::parse_ip("2001:db8::1").unwrap();
250///
251/// // Parse IPv6 with zone identifier
252/// let ipv6_zone = crypto_ipcrypt::parse_ip("fe80::1%eth0").unwrap();
253/// ```
254pub fn parse_ip(ip: &str) -> Result<[u8; BYTES], SodiumError> {
255    let mut bin = [0u8; BYTES];
256    let ret = unsafe {
257        libsodium_sys::sodium_ip2bin(
258            bin.as_mut_ptr(),
259            ip.as_ptr() as *const std::os::raw::c_char,
260            ip.len(),
261        )
262    };
263    if ret == 0 {
264        Ok(bin)
265    } else {
266        Err(SodiumError::InvalidInput("invalid IP address".into()))
267    }
268}
269
270/// Formats a 16-byte binary IP address as a string
271///
272/// IPv4-mapped addresses (::ffff:a.b.c.d) are converted to dotted-decimal notation.
273/// IPv6 addresses are formatted in standard compressed notation.
274///
275/// This uses libsodium's `sodium_bin2ip()` function internally.
276///
277/// # Example
278///
279/// ```rust
280/// use libsodium_rs::crypto_ipcrypt;
281///
282/// let bin = crypto_ipcrypt::parse_ip("192.0.2.1").unwrap();
283/// let formatted = crypto_ipcrypt::format_ip(&bin);
284/// assert_eq!(formatted, "192.0.2.1");
285///
286/// let ipv6 = crypto_ipcrypt::parse_ip("2001:db8::1").unwrap();
287/// let formatted = crypto_ipcrypt::format_ip(&ipv6);
288/// assert_eq!(formatted, "2001:db8::1");
289/// ```
290pub fn format_ip(bin: &[u8; BYTES]) -> String {
291    let mut buf = [0i8; IP_MAXLEN];
292    let ptr = unsafe {
293        libsodium_sys::sodium_bin2ip(
294            buf.as_mut_ptr() as *mut std::os::raw::c_char,
295            IP_MAXLEN,
296            bin.as_ptr(),
297        )
298    };
299    if ptr.is_null() {
300        // This shouldn't happen for valid 16-byte input, but handle it anyway
301        String::new()
302    } else {
303        // Find the null terminator and convert to String
304        let len = buf.iter().position(|&c| c == 0).unwrap_or(IP_MAXLEN);
305        let bytes: Vec<u8> = buf[..len].iter().map(|&c| c as u8).collect();
306        String::from_utf8_lossy(&bytes).into_owned()
307    }
308}
309
310/// Encrypts an IP address using deterministic encryption
311///
312/// The same address with the same key always produces the same ciphertext.
313/// This is useful for rate limiting, deduplication, and database indexing.
314///
315/// # Arguments
316///
317/// * `input` - The 16-byte IP address to encrypt
318/// * `key` - The 16-byte encryption key
319///
320/// # Returns
321///
322/// The 16-byte encrypted IP address
323pub fn encrypt(input: &[u8; BYTES], key: &Key) -> [u8; BYTES] {
324    let mut output = [0u8; BYTES];
325    unsafe {
326        libsodium_sys::crypto_ipcrypt_encrypt(output.as_mut_ptr(), input.as_ptr(), key.0.as_ptr());
327    }
328    output
329}
330
331/// Decrypts an IP address encrypted with deterministic encryption
332///
333/// # Arguments
334///
335/// * `input` - The 16-byte encrypted IP address
336/// * `key` - The 16-byte encryption key
337///
338/// # Returns
339///
340/// The 16-byte decrypted IP address
341pub fn decrypt(input: &[u8; BYTES], key: &Key) -> [u8; BYTES] {
342    let mut output = [0u8; BYTES];
343    unsafe {
344        libsodium_sys::crypto_ipcrypt_decrypt(output.as_mut_ptr(), input.as_ptr(), key.0.as_ptr());
345    }
346    output
347}
348
349/// Encrypts an IP address string using deterministic encryption
350///
351/// This is a convenience function that parses the IP, encrypts it, and returns
352/// the encrypted address as a formatted IP string.
353///
354/// # Example
355///
356/// ```rust
357/// use libsodium_rs::crypto_ipcrypt;
358///
359/// let key = crypto_ipcrypt::Key::generate();
360/// let encrypted = crypto_ipcrypt::encrypt_str("192.0.2.1", &key).unwrap();
361/// let decrypted = crypto_ipcrypt::decrypt_str(&encrypted, &key).unwrap();
362/// assert_eq!(decrypted, "192.0.2.1");
363/// ```
364pub fn encrypt_str(ip: &str, key: &Key) -> Result<String, SodiumError> {
365    let bin = parse_ip(ip)?;
366    let encrypted = encrypt(&bin, key);
367    Ok(format_ip(&encrypted))
368}
369
370/// Decrypts an IP address string encrypted with deterministic encryption
371///
372/// # Example
373///
374/// ```rust
375/// use libsodium_rs::crypto_ipcrypt;
376///
377/// let key = crypto_ipcrypt::Key::generate();
378/// let encrypted = crypto_ipcrypt::encrypt_str("2001:db8::1", &key).unwrap();
379/// let decrypted = crypto_ipcrypt::decrypt_str(&encrypted, &key).unwrap();
380/// assert_eq!(decrypted, "2001:db8::1");
381/// ```
382pub fn decrypt_str(ip: &str, key: &Key) -> Result<String, SodiumError> {
383    let bin = parse_ip(ip)?;
384    let decrypted = decrypt(&bin, key);
385    Ok(format_ip(&decrypted))
386}
387
388/// Non-deterministic IP address encryption (ND variant)
389///
390/// Uses a random 8-byte tweak to ensure the same address produces different
391/// ciphertexts each time. Good for log archival and third-party analytics.
392pub mod nd {
393    use super::*;
394
395    /// Number of bytes in an ND key (16)
396    pub const KEYBYTES: usize = libsodium_sys::crypto_ipcrypt_ND_KEYBYTES as usize;
397
398    /// Number of bytes in an ND tweak (8)
399    pub const TWEAKBYTES: usize = libsodium_sys::crypto_ipcrypt_ND_TWEAKBYTES as usize;
400
401    /// Number of bytes in ND input (16)
402    pub const INPUTBYTES: usize = libsodium_sys::crypto_ipcrypt_ND_INPUTBYTES as usize;
403
404    /// Number of bytes in ND output (24)
405    pub const OUTPUTBYTES: usize = libsodium_sys::crypto_ipcrypt_ND_OUTPUTBYTES as usize;
406
407    /// A tweak for non-deterministic encryption
408    #[derive(Clone, Copy)]
409    pub struct Tweak([u8; TWEAKBYTES]);
410
411    impl Tweak {
412        /// Generates a random tweak
413        pub fn random() -> Self {
414            let mut tweak = [0u8; TWEAKBYTES];
415            random::fill_bytes(&mut tweak);
416            Tweak(tweak)
417        }
418
419        /// Creates a tweak from a byte slice
420        pub fn from_slice(slice: &[u8]) -> Self {
421            assert_eq!(slice.len(), TWEAKBYTES, "tweak must be {TWEAKBYTES} bytes");
422            let mut tweak = [0u8; TWEAKBYTES];
423            tweak.copy_from_slice(slice);
424            Tweak(tweak)
425        }
426
427        /// Returns the tweak as a byte slice
428        pub fn as_bytes(&self) -> &[u8] {
429            &self.0
430        }
431    }
432
433    impl From<[u8; TWEAKBYTES]> for Tweak {
434        fn from(bytes: [u8; TWEAKBYTES]) -> Self {
435            Tweak(bytes)
436        }
437    }
438
439    /// Encrypts an IP address using non-deterministic encryption
440    ///
441    /// The output includes the tweak prepended to the ciphertext, so no separate
442    /// tweak storage is needed for decryption.
443    ///
444    /// # Arguments
445    ///
446    /// * `input` - The 16-byte IP address to encrypt
447    /// * `tweak` - The 8-byte random tweak
448    /// * `key` - The 16-byte encryption key
449    ///
450    /// # Returns
451    ///
452    /// The 24-byte encrypted IP address (tweak + ciphertext)
453    pub fn encrypt(input: &[u8; INPUTBYTES], tweak: &Tweak, key: &super::Key) -> [u8; OUTPUTBYTES] {
454        let mut output = [0u8; OUTPUTBYTES];
455        unsafe {
456            libsodium_sys::crypto_ipcrypt_nd_encrypt(
457                output.as_mut_ptr(),
458                input.as_ptr(),
459                tweak.0.as_ptr(),
460                key.0.as_ptr(),
461            );
462        }
463        output
464    }
465
466    /// Decrypts an IP address encrypted with non-deterministic encryption
467    ///
468    /// The tweak is extracted from the ciphertext automatically.
469    ///
470    /// # Arguments
471    ///
472    /// * `input` - The 24-byte encrypted IP address
473    /// * `key` - The 16-byte encryption key
474    ///
475    /// # Returns
476    ///
477    /// The 16-byte decrypted IP address
478    pub fn decrypt(input: &[u8; OUTPUTBYTES], key: &super::Key) -> [u8; INPUTBYTES] {
479        let mut output = [0u8; INPUTBYTES];
480        unsafe {
481            libsodium_sys::crypto_ipcrypt_nd_decrypt(
482                output.as_mut_ptr(),
483                input.as_ptr(),
484                key.0.as_ptr(),
485            );
486        }
487        output
488    }
489
490    /// Encrypts an IP address string using non-deterministic encryption
491    ///
492    /// Returns the 24-byte ciphertext as a hex string (48 characters).
493    ///
494    /// # Example
495    ///
496    /// ```rust
497    /// use libsodium_rs::crypto_ipcrypt;
498    ///
499    /// let key = crypto_ipcrypt::Key::generate();
500    /// let encrypted = crypto_ipcrypt::nd::encrypt_str("192.0.2.1", &key).unwrap();
501    /// assert_eq!(encrypted.len(), 48); // 24 bytes as hex
502    ///
503    /// let decrypted = crypto_ipcrypt::nd::decrypt_str(&encrypted, &key).unwrap();
504    /// assert_eq!(decrypted, "192.0.2.1");
505    /// ```
506    pub fn encrypt_str(ip: &str, key: &super::Key) -> Result<String, SodiumError> {
507        let bin = super::parse_ip(ip)?;
508        let tweak = Tweak::random();
509        let encrypted = encrypt(&bin, &tweak, key);
510        Ok(crate::utils::bin2hex(&encrypted))
511    }
512
513    /// Decrypts a hex-encoded ND ciphertext and returns the IP address as a string
514    ///
515    /// # Example
516    ///
517    /// ```rust
518    /// use libsodium_rs::crypto_ipcrypt;
519    ///
520    /// let key = crypto_ipcrypt::Key::generate();
521    /// let encrypted = crypto_ipcrypt::nd::encrypt_str("2001:db8::1", &key).unwrap();
522    /// let decrypted = crypto_ipcrypt::nd::decrypt_str(&encrypted, &key).unwrap();
523    /// assert_eq!(decrypted, "2001:db8::1");
524    /// ```
525    pub fn decrypt_str(hex: &str, key: &super::Key) -> Result<String, SodiumError> {
526        let bytes = crate::utils::hex2bin(hex)?;
527        if bytes.len() != OUTPUTBYTES {
528            return Err(SodiumError::InvalidInput(format!(
529                "expected {} bytes, got {}",
530                OUTPUTBYTES,
531                bytes.len()
532            )));
533        }
534        let mut input = [0u8; OUTPUTBYTES];
535        input.copy_from_slice(&bytes);
536        let decrypted = decrypt(&input, key);
537        Ok(super::format_ip(&decrypted))
538    }
539
540    /// Encodes a 24-byte ND ciphertext as a hex string
541    pub fn to_hex(ciphertext: &[u8; OUTPUTBYTES]) -> String {
542        crate::utils::bin2hex(ciphertext)
543    }
544
545    /// Decodes a hex string to a 24-byte ND ciphertext
546    pub fn from_hex(hex: &str) -> Result<[u8; OUTPUTBYTES], SodiumError> {
547        let bytes = crate::utils::hex2bin(hex)?;
548        if bytes.len() != OUTPUTBYTES {
549            return Err(SodiumError::InvalidInput(format!(
550                "expected {} bytes, got {}",
551                OUTPUTBYTES,
552                bytes.len()
553            )));
554        }
555        let mut output = [0u8; OUTPUTBYTES];
556        output.copy_from_slice(&bytes);
557        Ok(output)
558    }
559}
560
561/// Extended non-deterministic IP address encryption (NDX variant)
562///
563/// Uses a 32-byte key and 16-byte tweak for maximum security. The larger tweak
564/// space provides a higher birthday bound (~2^64), suitable for billions of
565/// encryptions per key.
566pub mod ndx {
567    use super::*;
568
569    /// Number of bytes in an NDX key (32)
570    pub const KEYBYTES: usize = libsodium_sys::crypto_ipcrypt_NDX_KEYBYTES as usize;
571
572    /// Number of bytes in an NDX tweak (16)
573    pub const TWEAKBYTES: usize = libsodium_sys::crypto_ipcrypt_NDX_TWEAKBYTES as usize;
574
575    /// Number of bytes in NDX input (16)
576    pub const INPUTBYTES: usize = libsodium_sys::crypto_ipcrypt_NDX_INPUTBYTES as usize;
577
578    /// Number of bytes in NDX output (32)
579    pub const OUTPUTBYTES: usize = libsodium_sys::crypto_ipcrypt_NDX_OUTPUTBYTES as usize;
580
581    /// A key for NDX encryption
582    #[derive(Clone, zeroize::Zeroize, zeroize::ZeroizeOnDrop)]
583    pub struct Key([u8; KEYBYTES]);
584
585    impl Key {
586        /// Generates a new random key
587        pub fn generate() -> Self {
588            let mut key = [0u8; KEYBYTES];
589            unsafe {
590                libsodium_sys::crypto_ipcrypt_ndx_keygen(key.as_mut_ptr());
591            }
592            Key(key)
593        }
594
595        /// Creates a key from a byte slice
596        pub fn from_slice(slice: &[u8]) -> Self {
597            assert_eq!(slice.len(), KEYBYTES, "key must be {KEYBYTES} bytes");
598            let mut key = [0u8; KEYBYTES];
599            key.copy_from_slice(slice);
600            Key(key)
601        }
602
603        /// Returns the key as a byte slice
604        pub fn as_bytes(&self) -> &[u8] {
605            &self.0
606        }
607    }
608
609    impl AsRef<[u8]> for Key {
610        fn as_ref(&self) -> &[u8] {
611            &self.0
612        }
613    }
614
615    impl From<[u8; KEYBYTES]> for Key {
616        fn from(bytes: [u8; KEYBYTES]) -> Self {
617            Key(bytes)
618        }
619    }
620
621    /// A tweak for NDX encryption
622    #[derive(Clone, Copy)]
623    pub struct Tweak([u8; TWEAKBYTES]);
624
625    impl Tweak {
626        /// Generates a random tweak
627        pub fn random() -> Self {
628            let mut tweak = [0u8; TWEAKBYTES];
629            random::fill_bytes(&mut tweak);
630            Tweak(tweak)
631        }
632
633        /// Creates a tweak from a byte slice
634        pub fn from_slice(slice: &[u8]) -> Self {
635            assert_eq!(slice.len(), TWEAKBYTES, "tweak must be {TWEAKBYTES} bytes");
636            let mut tweak = [0u8; TWEAKBYTES];
637            tweak.copy_from_slice(slice);
638            Tweak(tweak)
639        }
640
641        /// Returns the tweak as a byte slice
642        pub fn as_bytes(&self) -> &[u8] {
643            &self.0
644        }
645    }
646
647    impl From<[u8; TWEAKBYTES]> for Tweak {
648        fn from(bytes: [u8; TWEAKBYTES]) -> Self {
649            Tweak(bytes)
650        }
651    }
652
653    /// Encrypts an IP address using extended non-deterministic encryption
654    ///
655    /// # Arguments
656    ///
657    /// * `input` - The 16-byte IP address to encrypt
658    /// * `tweak` - The 16-byte random tweak
659    /// * `key` - The 32-byte encryption key
660    ///
661    /// # Returns
662    ///
663    /// The 32-byte encrypted IP address
664    pub fn encrypt(input: &[u8; INPUTBYTES], tweak: &Tweak, key: &Key) -> [u8; OUTPUTBYTES] {
665        let mut output = [0u8; OUTPUTBYTES];
666        unsafe {
667            libsodium_sys::crypto_ipcrypt_ndx_encrypt(
668                output.as_mut_ptr(),
669                input.as_ptr(),
670                tweak.0.as_ptr(),
671                key.0.as_ptr(),
672            );
673        }
674        output
675    }
676
677    /// Decrypts an IP address encrypted with extended non-deterministic encryption
678    ///
679    /// # Arguments
680    ///
681    /// * `input` - The 32-byte encrypted IP address
682    /// * `key` - The 32-byte encryption key
683    ///
684    /// # Returns
685    ///
686    /// The 16-byte decrypted IP address
687    pub fn decrypt(input: &[u8; OUTPUTBYTES], key: &Key) -> [u8; INPUTBYTES] {
688        let mut output = [0u8; INPUTBYTES];
689        unsafe {
690            libsodium_sys::crypto_ipcrypt_ndx_decrypt(
691                output.as_mut_ptr(),
692                input.as_ptr(),
693                key.0.as_ptr(),
694            );
695        }
696        output
697    }
698
699    /// Encrypts an IP address string using extended non-deterministic encryption
700    ///
701    /// Returns the 32-byte ciphertext as a hex string (64 characters).
702    ///
703    /// # Example
704    ///
705    /// ```rust
706    /// use libsodium_rs::crypto_ipcrypt;
707    ///
708    /// let key = crypto_ipcrypt::ndx::Key::generate();
709    /// let encrypted = crypto_ipcrypt::ndx::encrypt_str("192.0.2.1", &key).unwrap();
710    /// assert_eq!(encrypted.len(), 64); // 32 bytes as hex
711    ///
712    /// let decrypted = crypto_ipcrypt::ndx::decrypt_str(&encrypted, &key).unwrap();
713    /// assert_eq!(decrypted, "192.0.2.1");
714    /// ```
715    pub fn encrypt_str(ip: &str, key: &Key) -> Result<String, SodiumError> {
716        let bin = super::parse_ip(ip)?;
717        let tweak = Tweak::random();
718        let encrypted = encrypt(&bin, &tweak, key);
719        Ok(crate::utils::bin2hex(&encrypted))
720    }
721
722    /// Decrypts a hex-encoded NDX ciphertext and returns the IP address as a string
723    ///
724    /// # Example
725    ///
726    /// ```rust
727    /// use libsodium_rs::crypto_ipcrypt;
728    ///
729    /// let key = crypto_ipcrypt::ndx::Key::generate();
730    /// let encrypted = crypto_ipcrypt::ndx::encrypt_str("2001:db8::1", &key).unwrap();
731    /// let decrypted = crypto_ipcrypt::ndx::decrypt_str(&encrypted, &key).unwrap();
732    /// assert_eq!(decrypted, "2001:db8::1");
733    /// ```
734    pub fn decrypt_str(hex: &str, key: &Key) -> Result<String, SodiumError> {
735        let bytes = crate::utils::hex2bin(hex)?;
736        if bytes.len() != OUTPUTBYTES {
737            return Err(SodiumError::InvalidInput(format!(
738                "expected {} bytes, got {}",
739                OUTPUTBYTES,
740                bytes.len()
741            )));
742        }
743        let mut input = [0u8; OUTPUTBYTES];
744        input.copy_from_slice(&bytes);
745        let decrypted = decrypt(&input, key);
746        Ok(super::format_ip(&decrypted))
747    }
748
749    /// Encodes a 32-byte NDX ciphertext as a hex string
750    pub fn to_hex(ciphertext: &[u8; OUTPUTBYTES]) -> String {
751        crate::utils::bin2hex(ciphertext)
752    }
753
754    /// Decodes a hex string to a 32-byte NDX ciphertext
755    pub fn from_hex(hex: &str) -> Result<[u8; OUTPUTBYTES], SodiumError> {
756        let bytes = crate::utils::hex2bin(hex)?;
757        if bytes.len() != OUTPUTBYTES {
758            return Err(SodiumError::InvalidInput(format!(
759                "expected {} bytes, got {}",
760                OUTPUTBYTES,
761                bytes.len()
762            )));
763        }
764        let mut output = [0u8; OUTPUTBYTES];
765        output.copy_from_slice(&bytes);
766        Ok(output)
767    }
768}
769
770/// Prefix-preserving IP address encryption (PFX variant)
771///
772/// Preserves network prefix relationships: addresses sharing a common prefix
773/// will have ciphertexts sharing a corresponding (different) prefix.
774///
775/// Use cases:
776/// - Network research and academic datasets
777/// - DDoS attack analysis
778/// - PCAP and NetFlow anonymization
779/// - ISP and CDN traffic analysis
780pub mod pfx {
781
782    /// Number of bytes in a PFX key (32)
783    pub const KEYBYTES: usize = libsodium_sys::crypto_ipcrypt_PFX_KEYBYTES as usize;
784
785    /// Number of bytes in PFX input/output (16)
786    pub const BYTES: usize = libsodium_sys::crypto_ipcrypt_PFX_BYTES as usize;
787
788    /// A key for prefix-preserving encryption
789    #[derive(Clone, zeroize::Zeroize, zeroize::ZeroizeOnDrop)]
790    pub struct Key([u8; KEYBYTES]);
791
792    impl Key {
793        /// Generates a new random key
794        pub fn generate() -> Self {
795            let mut key = [0u8; KEYBYTES];
796            unsafe {
797                libsodium_sys::crypto_ipcrypt_pfx_keygen(key.as_mut_ptr());
798            }
799            Key(key)
800        }
801
802        /// Creates a key from a byte slice
803        pub fn from_slice(slice: &[u8]) -> Self {
804            assert_eq!(slice.len(), KEYBYTES, "key must be {KEYBYTES} bytes");
805            let mut key = [0u8; KEYBYTES];
806            key.copy_from_slice(slice);
807            Key(key)
808        }
809
810        /// Returns the key as a byte slice
811        pub fn as_bytes(&self) -> &[u8] {
812            &self.0
813        }
814    }
815
816    impl AsRef<[u8]> for Key {
817        fn as_ref(&self) -> &[u8] {
818            &self.0
819        }
820    }
821
822    impl From<[u8; KEYBYTES]> for Key {
823        fn from(bytes: [u8; KEYBYTES]) -> Self {
824            Key(bytes)
825        }
826    }
827
828    /// Encrypts an IP address using prefix-preserving encryption
829    ///
830    /// Addresses in the same subnet will produce ciphertexts in a corresponding
831    /// (encrypted) subnet. The address family is preserved: IPv4 addresses
832    /// encrypt to IPv4 addresses.
833    ///
834    /// # Arguments
835    ///
836    /// * `input` - The 16-byte IP address to encrypt
837    /// * `key` - The 32-byte encryption key
838    ///
839    /// # Returns
840    ///
841    /// The 16-byte encrypted IP address
842    pub fn encrypt(input: &[u8; BYTES], key: &Key) -> [u8; BYTES] {
843        let mut output = [0u8; BYTES];
844        unsafe {
845            libsodium_sys::crypto_ipcrypt_pfx_encrypt(
846                output.as_mut_ptr(),
847                input.as_ptr(),
848                key.0.as_ptr(),
849            );
850        }
851        output
852    }
853
854    /// Decrypts an IP address encrypted with prefix-preserving encryption
855    ///
856    /// # Arguments
857    ///
858    /// * `input` - The 16-byte encrypted IP address
859    /// * `key` - The 32-byte encryption key
860    ///
861    /// # Returns
862    ///
863    /// The 16-byte decrypted IP address
864    pub fn decrypt(input: &[u8; BYTES], key: &Key) -> [u8; BYTES] {
865        let mut output = [0u8; BYTES];
866        unsafe {
867            libsodium_sys::crypto_ipcrypt_pfx_decrypt(
868                output.as_mut_ptr(),
869                input.as_ptr(),
870                key.0.as_ptr(),
871            );
872        }
873        output
874    }
875
876    /// Encrypts an IP address string using prefix-preserving encryption
877    ///
878    /// Returns the encrypted address as a formatted IP string.
879    ///
880    /// # Example
881    ///
882    /// ```rust
883    /// use libsodium_rs::crypto_ipcrypt;
884    ///
885    /// let key = crypto_ipcrypt::pfx::Key::generate();
886    /// let encrypted = crypto_ipcrypt::pfx::encrypt_str("192.0.2.1", &key).unwrap();
887    /// let decrypted = crypto_ipcrypt::pfx::decrypt_str(&encrypted, &key).unwrap();
888    /// assert_eq!(decrypted, "192.0.2.1");
889    /// ```
890    pub fn encrypt_str(ip: &str, key: &Key) -> Result<String, super::SodiumError> {
891        let bin = super::parse_ip(ip)?;
892        let encrypted = encrypt(&bin, key);
893        Ok(super::format_ip(&encrypted))
894    }
895
896    /// Decrypts an IP address string encrypted with prefix-preserving encryption
897    ///
898    /// # Example
899    ///
900    /// ```rust
901    /// use libsodium_rs::crypto_ipcrypt;
902    ///
903    /// let key = crypto_ipcrypt::pfx::Key::generate();
904    /// let encrypted = crypto_ipcrypt::pfx::encrypt_str("2001:db8::1", &key).unwrap();
905    /// let decrypted = crypto_ipcrypt::pfx::decrypt_str(&encrypted, &key).unwrap();
906    /// assert_eq!(decrypted, "2001:db8::1");
907    /// ```
908    pub fn decrypt_str(ip: &str, key: &Key) -> Result<String, super::SodiumError> {
909        let bin = super::parse_ip(ip)?;
910        let decrypted = decrypt(&bin, key);
911        Ok(super::format_ip(&decrypted))
912    }
913}
914
915#[cfg(test)]
916mod tests {
917    use super::{
918        bytes_to_ipv4, decrypt, encrypt, ipv4_to_bytes, nd, ndx, pfx, Key, BYTES, KEYBYTES,
919    };
920
921    #[test]
922    fn test_ipv4_conversion() {
923        let ipv4 = [192, 0, 2, 1];
924        let bytes = ipv4_to_bytes(ipv4);
925
926        // Check IPv4-mapped format
927        assert!(bytes[..10].iter().all(|&b| b == 0));
928        assert_eq!(bytes[10], 0xff);
929        assert_eq!(bytes[11], 0xff);
930        assert_eq!(&bytes[12..], &ipv4);
931
932        // Convert back
933        let recovered = bytes_to_ipv4(&bytes).unwrap();
934        assert_eq!(recovered, ipv4);
935    }
936
937    #[test]
938    fn test_deterministic_encryption() {
939        let key = Key::generate();
940        let ip = ipv4_to_bytes([192, 0, 2, 1]);
941
942        let encrypted = encrypt(&ip, &key);
943        let decrypted = decrypt(&encrypted, &key);
944
945        assert_eq!(ip, decrypted);
946        assert_ne!(ip, encrypted);
947    }
948
949    #[test]
950    fn test_deterministic_same_input_same_output() {
951        let key = Key::generate();
952        let ip = ipv4_to_bytes([10, 0, 0, 1]);
953
954        let encrypted1 = encrypt(&ip, &key);
955        let encrypted2 = encrypt(&ip, &key);
956
957        assert_eq!(encrypted1, encrypted2);
958    }
959
960    #[test]
961    fn test_nd_encryption() {
962        let key = Key::generate();
963        let ip = ipv4_to_bytes([192, 168, 1, 1]);
964        let tweak = nd::Tweak::random();
965
966        let encrypted = nd::encrypt(&ip, &tweak, &key);
967        let decrypted = nd::decrypt(&encrypted, &key);
968
969        assert_eq!(ip, decrypted);
970        assert_eq!(encrypted.len(), nd::OUTPUTBYTES);
971    }
972
973    #[test]
974    fn test_nd_different_tweaks() {
975        let key = Key::generate();
976        let ip = ipv4_to_bytes([192, 168, 1, 1]);
977
978        let tweak1 = nd::Tweak::random();
979        let tweak2 = nd::Tweak::random();
980
981        let encrypted1 = nd::encrypt(&ip, &tweak1, &key);
982        let encrypted2 = nd::encrypt(&ip, &tweak2, &key);
983
984        // Different tweaks should produce different ciphertexts
985        assert_ne!(encrypted1, encrypted2);
986
987        // Both should decrypt to the same value
988        let decrypted1 = nd::decrypt(&encrypted1, &key);
989        let decrypted2 = nd::decrypt(&encrypted2, &key);
990
991        assert_eq!(decrypted1, ip);
992        assert_eq!(decrypted2, ip);
993    }
994
995    #[test]
996    fn test_ndx_encryption() {
997        let key = ndx::Key::generate();
998        let ip = ipv4_to_bytes([172, 16, 0, 1]);
999        let tweak = ndx::Tweak::random();
1000
1001        let encrypted = ndx::encrypt(&ip, &tweak, &key);
1002        let decrypted = ndx::decrypt(&encrypted, &key);
1003
1004        assert_eq!(ip, decrypted);
1005        assert_eq!(encrypted.len(), ndx::OUTPUTBYTES);
1006    }
1007
1008    #[test]
1009    fn test_ndx_different_tweaks() {
1010        let key = ndx::Key::generate();
1011        let ip = ipv4_to_bytes([172, 16, 0, 1]);
1012
1013        let tweak1 = ndx::Tweak::random();
1014        let tweak2 = ndx::Tweak::random();
1015
1016        let encrypted1 = ndx::encrypt(&ip, &tweak1, &key);
1017        let encrypted2 = ndx::encrypt(&ip, &tweak2, &key);
1018
1019        assert_ne!(encrypted1, encrypted2);
1020
1021        assert_eq!(ndx::decrypt(&encrypted1, &key), ip);
1022        assert_eq!(ndx::decrypt(&encrypted2, &key), ip);
1023    }
1024
1025    #[test]
1026    fn test_pfx_encryption() {
1027        let key = pfx::Key::generate();
1028        let ip = ipv4_to_bytes([10, 0, 0, 1]);
1029
1030        let encrypted = pfx::encrypt(&ip, &key);
1031        let decrypted = pfx::decrypt(&encrypted, &key);
1032
1033        assert_eq!(ip, decrypted);
1034        assert_eq!(encrypted.len(), pfx::BYTES);
1035    }
1036
1037    #[test]
1038    fn test_pfx_preserves_relationship() {
1039        let key = pfx::Key::generate();
1040
1041        // Two addresses in the same /24
1042        let ip1 = ipv4_to_bytes([10, 0, 0, 1]);
1043        let ip2 = ipv4_to_bytes([10, 0, 0, 100]);
1044
1045        let enc1 = pfx::encrypt(&ip1, &key);
1046        let enc2 = pfx::encrypt(&ip2, &key);
1047
1048        // They should both decrypt correctly
1049        assert_eq!(pfx::decrypt(&enc1, &key), ip1);
1050        assert_eq!(pfx::decrypt(&enc2, &key), ip2);
1051
1052        // The encrypted addresses should share a common prefix
1053        // (We can't easily verify the exact prefix length, but we can
1054        // at least verify they're different)
1055        assert_ne!(enc1, enc2);
1056    }
1057
1058    #[test]
1059    fn test_different_keys_different_results() {
1060        let key1 = Key::generate();
1061        let key2 = Key::generate();
1062        let ip = ipv4_to_bytes([192, 0, 2, 1]);
1063
1064        let encrypted1 = encrypt(&ip, &key1);
1065        let encrypted2 = encrypt(&ip, &key2);
1066
1067        assert_ne!(encrypted1, encrypted2);
1068    }
1069
1070    #[test]
1071    fn test_key_from_slice() {
1072        let bytes = [0x42u8; KEYBYTES];
1073        let key = Key::from_slice(&bytes);
1074        assert_eq!(key.as_bytes(), &bytes);
1075    }
1076
1077    #[test]
1078    fn test_ndx_key_from_slice() {
1079        let bytes = [0x42u8; ndx::KEYBYTES];
1080        let key = ndx::Key::from_slice(&bytes);
1081        assert_eq!(key.as_bytes(), &bytes);
1082    }
1083
1084    #[test]
1085    fn test_pfx_key_from_slice() {
1086        let bytes = [0x42u8; pfx::KEYBYTES];
1087        let key = pfx::Key::from_slice(&bytes);
1088        assert_eq!(key.as_bytes(), &bytes);
1089    }
1090
1091    #[test]
1092    fn test_ipv6_encryption() {
1093        let key = Key::generate();
1094
1095        // An IPv6 address (2001:db8::1)
1096        let ip: [u8; BYTES] = [
1097            0x20, 0x01, 0x0d, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
1098            0x00, 0x01,
1099        ];
1100
1101        let encrypted = encrypt(&ip, &key);
1102        let decrypted = decrypt(&encrypted, &key);
1103
1104        assert_eq!(ip, decrypted);
1105
1106        // Should not be detected as IPv4-mapped
1107        assert!(bytes_to_ipv4(&ip).is_none());
1108    }
1109
1110    #[test]
1111    fn test_constants() {
1112        assert_eq!(BYTES, 16);
1113        assert_eq!(KEYBYTES, 16);
1114
1115        assert_eq!(nd::KEYBYTES, 16);
1116        assert_eq!(nd::TWEAKBYTES, 8);
1117        assert_eq!(nd::INPUTBYTES, 16);
1118        assert_eq!(nd::OUTPUTBYTES, 24);
1119
1120        assert_eq!(ndx::KEYBYTES, 32);
1121        assert_eq!(ndx::TWEAKBYTES, 16);
1122        assert_eq!(ndx::INPUTBYTES, 16);
1123        assert_eq!(ndx::OUTPUTBYTES, 32);
1124
1125        assert_eq!(pfx::KEYBYTES, 32);
1126        assert_eq!(pfx::BYTES, 16);
1127    }
1128
1129    #[test]
1130    fn test_parse_ip_ipv4() {
1131        use super::{format_ip, parse_ip};
1132
1133        let bin = parse_ip("192.0.2.1").unwrap();
1134        assert_eq!(format_ip(&bin), "192.0.2.1");
1135
1136        let bin = parse_ip("10.0.0.1").unwrap();
1137        assert_eq!(format_ip(&bin), "10.0.0.1");
1138
1139        let bin = parse_ip("255.255.255.255").unwrap();
1140        assert_eq!(format_ip(&bin), "255.255.255.255");
1141    }
1142
1143    #[test]
1144    fn test_parse_ip_ipv6() {
1145        use super::{format_ip, parse_ip};
1146
1147        let bin = parse_ip("2001:db8::1").unwrap();
1148        assert_eq!(format_ip(&bin), "2001:db8::1");
1149
1150        let bin = parse_ip("::1").unwrap();
1151        assert_eq!(format_ip(&bin), "::1");
1152
1153        let bin = parse_ip("fe80::1").unwrap();
1154        assert_eq!(format_ip(&bin), "fe80::1");
1155
1156        // Full IPv6 address
1157        let bin = parse_ip("2001:0db8:85a3:0000:0000:8a2e:0370:7334").unwrap();
1158        // libsodium compresses it
1159        let formatted = format_ip(&bin);
1160        assert!(formatted.contains("2001:"));
1161    }
1162
1163    #[test]
1164    fn test_parse_ip_invalid() {
1165        use super::parse_ip;
1166
1167        assert!(parse_ip("invalid").is_err());
1168        assert!(parse_ip("256.0.0.1").is_err());
1169        assert!(parse_ip("").is_err());
1170    }
1171
1172    #[test]
1173    fn test_encrypt_str_deterministic() {
1174        use super::{decrypt_str, encrypt_str, Key};
1175
1176        let key = Key::generate();
1177
1178        // IPv4
1179        let encrypted = encrypt_str("192.0.2.1", &key).unwrap();
1180        let decrypted = decrypt_str(&encrypted, &key).unwrap();
1181        assert_eq!(decrypted, "192.0.2.1");
1182
1183        // IPv6
1184        let encrypted = encrypt_str("2001:db8::1", &key).unwrap();
1185        let decrypted = decrypt_str(&encrypted, &key).unwrap();
1186        assert_eq!(decrypted, "2001:db8::1");
1187    }
1188
1189    #[test]
1190    fn test_nd_encrypt_str() {
1191        let key = Key::generate();
1192
1193        let encrypted = nd::encrypt_str("192.0.2.1", &key).unwrap();
1194        assert_eq!(encrypted.len(), nd::OUTPUTBYTES * 2); // hex encoded
1195
1196        let decrypted = nd::decrypt_str(&encrypted, &key).unwrap();
1197        assert_eq!(decrypted, "192.0.2.1");
1198
1199        // IPv6
1200        let encrypted = nd::encrypt_str("2001:db8::1", &key).unwrap();
1201        let decrypted = nd::decrypt_str(&encrypted, &key).unwrap();
1202        assert_eq!(decrypted, "2001:db8::1");
1203    }
1204
1205    #[test]
1206    fn test_nd_hex_encoding() {
1207        let key = Key::generate();
1208        let ip = ipv4_to_bytes([192, 0, 2, 1]);
1209        let tweak = nd::Tweak::random();
1210
1211        let encrypted = nd::encrypt(&ip, &tweak, &key);
1212        let hex = nd::to_hex(&encrypted);
1213        assert_eq!(hex.len(), nd::OUTPUTBYTES * 2);
1214
1215        let decoded = nd::from_hex(&hex).unwrap();
1216        assert_eq!(encrypted, decoded);
1217    }
1218
1219    #[test]
1220    fn test_ndx_encrypt_str() {
1221        let key = ndx::Key::generate();
1222
1223        let encrypted = ndx::encrypt_str("192.0.2.1", &key).unwrap();
1224        assert_eq!(encrypted.len(), ndx::OUTPUTBYTES * 2); // hex encoded
1225
1226        let decrypted = ndx::decrypt_str(&encrypted, &key).unwrap();
1227        assert_eq!(decrypted, "192.0.2.1");
1228
1229        // IPv6
1230        let encrypted = ndx::encrypt_str("2001:db8::1", &key).unwrap();
1231        let decrypted = ndx::decrypt_str(&encrypted, &key).unwrap();
1232        assert_eq!(decrypted, "2001:db8::1");
1233    }
1234
1235    #[test]
1236    fn test_ndx_hex_encoding() {
1237        let key = ndx::Key::generate();
1238        let ip = ipv4_to_bytes([192, 0, 2, 1]);
1239        let tweak = ndx::Tweak::random();
1240
1241        let encrypted = ndx::encrypt(&ip, &tweak, &key);
1242        let hex = ndx::to_hex(&encrypted);
1243        assert_eq!(hex.len(), ndx::OUTPUTBYTES * 2);
1244
1245        let decoded = ndx::from_hex(&hex).unwrap();
1246        assert_eq!(encrypted, decoded);
1247    }
1248
1249    #[test]
1250    fn test_pfx_encrypt_str() {
1251        let key = pfx::Key::generate();
1252
1253        let encrypted = pfx::encrypt_str("192.0.2.1", &key).unwrap();
1254        let decrypted = pfx::decrypt_str(&encrypted, &key).unwrap();
1255        assert_eq!(decrypted, "192.0.2.1");
1256
1257        // IPv6
1258        let encrypted = pfx::encrypt_str("2001:db8::1", &key).unwrap();
1259        let decrypted = pfx::decrypt_str(&encrypted, &key).unwrap();
1260        assert_eq!(decrypted, "2001:db8::1");
1261    }
1262
1263    #[test]
1264    fn test_format_ip_consistency_with_manual_conversion() {
1265        use super::{bytes_to_ipv4, format_ip, ipv4_to_bytes, parse_ip};
1266
1267        // Test that parse_ip produces the same result as ipv4_to_bytes for IPv4
1268        let manual = ipv4_to_bytes([192, 0, 2, 1]);
1269        let parsed = parse_ip("192.0.2.1").unwrap();
1270        assert_eq!(manual, parsed);
1271
1272        // Test that format_ip can recover the original
1273        let formatted = format_ip(&manual);
1274        assert_eq!(formatted, "192.0.2.1");
1275
1276        // Test bytes_to_ipv4 on result from parse_ip
1277        let ipv4 = bytes_to_ipv4(&parsed).unwrap();
1278        assert_eq!(ipv4, [192, 0, 2, 1]);
1279    }
1280}