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}