Skip to main content

txgate_crypto/
encryption.rs

1//! AEAD encryption for key material at rest.
2//!
3//! This module provides ChaCha20-Poly1305 AEAD encryption with Argon2id key derivation
4//! for protecting secret key material at rest.
5//!
6//! # Security Properties
7//!
8//! - **Authenticated Encryption**: ChaCha20-Poly1305 provides both confidentiality and
9//!   integrity protection. Any tampering with the ciphertext will be detected during decryption.
10//!
11//! - **Key Derivation**: Argon2id is used to derive encryption keys from passphrases,
12//!   providing resistance against both GPU/ASIC attacks (memory-hard) and side-channel
13//!   attacks (data-independent memory access in the second phase).
14//!
15//! - **Random Salt and Nonce**: Each encryption operation generates fresh random salt
16//!   and nonce using the operating system's secure RNG, ensuring that:
17//!   - The same passphrase produces different ciphertexts
18//!   - Nonce reuse is avoided
19//!
20//! - **Zeroization**: Derived encryption keys are zeroized immediately after use.
21//!
22//! # Encrypted Key Format
23//!
24//! The encrypted key is serialized as follows (77 bytes total):
25//!
26//! ```text
27//! ┌─────────────────────────────────────┐
28//! │ version: 1 (1 byte)                 │
29//! │ salt: [u8; 16]                      │
30//! │ nonce: [u8; 12]                     │
31//! │ ciphertext: [u8; 32]                │
32//! │ tag: [u8; 16]                       │
33//! └─────────────────────────────────────┘
34//! ```
35//!
36//! The ciphertext and tag are combined in the serialized format (48 bytes total for
37//! 32-byte plaintext + 16-byte authentication tag).
38//!
39//! # Example
40//!
41//! ```rust
42//! use txgate_crypto::keys::SecretKey;
43//! use txgate_crypto::encryption::{encrypt_key, decrypt_key};
44//!
45//! // Generate a key to encrypt
46//! let secret_key = SecretKey::generate();
47//! let passphrase = "my secure passphrase";
48//!
49//! // Encrypt the key
50//! let encrypted = encrypt_key(&secret_key, passphrase).expect("encryption failed");
51//!
52//! // Serialize for storage
53//! let bytes = encrypted.to_bytes();
54//!
55//! // Later, deserialize and decrypt
56//! use txgate_crypto::encryption::EncryptedKey;
57//! let encrypted = EncryptedKey::from_bytes(&bytes).expect("invalid format");
58//! let decrypted = decrypt_key(&encrypted, passphrase).expect("decryption failed");
59//! ```
60//!
61//! # Security Considerations
62//!
63//! - Use strong, unique passphrases for each key
64//! - Store the encrypted key file with appropriate file system permissions
65//! - The passphrase should be obtained securely (e.g., from a secure input mechanism)
66//! - Do not log or display the passphrase or decrypted key material
67
68use argon2::{Algorithm, Argon2, Params, Version};
69use chacha20poly1305::{
70    aead::{Aead, KeyInit},
71    ChaCha20Poly1305, Nonce,
72};
73use rand::RngCore;
74use txgate_core::error::StoreError;
75use zeroize::{Zeroize, Zeroizing};
76
77use crate::keys::SecretKey;
78
79// ============================================================================
80// Constants
81// ============================================================================
82
83/// Current encryption format version.
84///
85/// This allows for future format changes while maintaining backward compatibility.
86pub const ENCRYPTION_VERSION: u8 = 1;
87
88/// Length of the salt in bytes.
89pub const SALT_LEN: usize = 16;
90
91/// Length of the nonce in bytes.
92pub const NONCE_LEN: usize = 12;
93
94/// Length of the authentication tag in bytes.
95pub const TAG_LEN: usize = 16;
96
97/// Length of the plaintext secret key in bytes.
98pub const PLAINTEXT_LEN: usize = 32;
99
100/// Total length of the encrypted key file in bytes.
101///
102/// Layout: version (1) + salt (16) + nonce (12) + ciphertext (32) + tag (16) = 77
103pub const ENCRYPTED_KEY_LEN: usize = 1 + SALT_LEN + NONCE_LEN + PLAINTEXT_LEN + TAG_LEN;
104
105// Argon2id parameters (OWASP recommended for password hashing)
106// See: https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html
107const ARGON2_MEMORY_KIB: u32 = 65536; // 64 MiB
108const ARGON2_ITERATIONS: u32 = 3;
109const ARGON2_PARALLELISM: u32 = 4;
110const ARGON2_OUTPUT_LEN: usize = 32;
111
112// ============================================================================
113// Types
114// ============================================================================
115
116/// An encrypted secret key container.
117///
118/// This structure holds all the data needed to decrypt a secret key:
119/// - Version byte for format compatibility
120/// - Salt used for key derivation
121/// - Nonce used for encryption
122/// - Ciphertext containing the encrypted key material and authentication tag
123///
124/// # Security
125///
126/// The salt and nonce are randomly generated for each encryption operation.
127/// The ciphertext includes a 16-byte authentication tag appended by ChaCha20-Poly1305.
128#[derive(Debug, Clone)]
129pub struct EncryptedKey {
130    /// Format version (currently always 1).
131    pub version: u8,
132    /// Random salt used for Argon2id key derivation.
133    pub salt: [u8; SALT_LEN],
134    /// Random nonce used for ChaCha20-Poly1305 encryption.
135    pub nonce: [u8; NONCE_LEN],
136    /// Encrypted key material with authentication tag (48 bytes: 32 + 16).
137    pub ciphertext: Vec<u8>,
138}
139
140impl EncryptedKey {
141    /// Serialize the encrypted key to bytes.
142    ///
143    /// The output is 77 bytes in the format:
144    /// `version || salt || nonce || ciphertext || tag`
145    ///
146    /// # Example
147    ///
148    /// ```rust
149    /// use txgate_crypto::keys::SecretKey;
150    /// use txgate_crypto::encryption::encrypt_key;
151    ///
152    /// let secret_key = SecretKey::generate();
153    /// let encrypted = encrypt_key(&secret_key, "passphrase").expect("encryption failed");
154    ///
155    /// let bytes = encrypted.to_bytes();
156    /// assert_eq!(bytes.len(), 77);
157    /// ```
158    #[must_use]
159    pub fn to_bytes(&self) -> Vec<u8> {
160        let mut bytes = Vec::with_capacity(ENCRYPTED_KEY_LEN);
161        bytes.push(self.version);
162        bytes.extend_from_slice(&self.salt);
163        bytes.extend_from_slice(&self.nonce);
164        bytes.extend_from_slice(&self.ciphertext);
165        bytes
166    }
167
168    /// Deserialize an encrypted key from bytes.
169    ///
170    /// # Errors
171    ///
172    /// Returns `StoreError::InvalidFormat` if:
173    /// - The input length is not exactly 77 bytes
174    /// - The version byte is not recognized
175    ///
176    /// # Example
177    ///
178    /// ```rust
179    /// use txgate_crypto::encryption::EncryptedKey;
180    ///
181    /// // Invalid length will return an error
182    /// let result = EncryptedKey::from_bytes(&[0u8; 10]);
183    /// assert!(result.is_err());
184    /// ```
185    pub fn from_bytes(bytes: &[u8]) -> Result<Self, StoreError> {
186        if bytes.len() != ENCRYPTED_KEY_LEN {
187            return Err(StoreError::InvalidFormat);
188        }
189
190        // Parse version (safe: length already validated)
191        let version = *bytes.first().ok_or(StoreError::InvalidFormat)?;
192        if version != ENCRYPTION_VERSION {
193            return Err(StoreError::InvalidFormat);
194        }
195
196        // Define byte ranges (safe: length already validated as ENCRYPTED_KEY_LEN = 77)
197        let salt_start = 1;
198        let salt_end = salt_start + SALT_LEN;
199        let nonce_start = salt_end;
200        let nonce_end = nonce_start + NONCE_LEN;
201        let ciphertext_start = nonce_end;
202
203        // Parse salt (safe: using .get() with validated ranges)
204        let salt_slice = bytes
205            .get(salt_start..salt_end)
206            .ok_or(StoreError::InvalidFormat)?;
207        let salt: [u8; SALT_LEN] = salt_slice
208            .try_into()
209            .map_err(|_| StoreError::InvalidFormat)?;
210
211        // Parse nonce (safe: using .get() with validated ranges)
212        let nonce_slice = bytes
213            .get(nonce_start..nonce_end)
214            .ok_or(StoreError::InvalidFormat)?;
215        let nonce: [u8; NONCE_LEN] = nonce_slice
216            .try_into()
217            .map_err(|_| StoreError::InvalidFormat)?;
218
219        // Parse ciphertext (safe: using .get() with validated range)
220        let ciphertext = bytes
221            .get(ciphertext_start..)
222            .ok_or(StoreError::InvalidFormat)?
223            .to_vec();
224
225        Ok(Self {
226            version,
227            salt,
228            nonce,
229            ciphertext,
230        })
231    }
232}
233
234// ============================================================================
235// Key Derivation
236// ============================================================================
237
238/// Derive an encryption key from a passphrase using Argon2id.
239///
240/// # Arguments
241///
242/// * `passphrase` - The user's passphrase
243/// * `salt` - A 16-byte random salt
244///
245/// # Returns
246///
247/// A 32-byte derived key suitable for use with ChaCha20-Poly1305.
248///
249/// # Security
250///
251/// - Uses Argon2id with OWASP-recommended parameters:
252///   - Memory: 64 MiB
253///   - Iterations: 3
254///   - Parallelism: 4
255/// - The caller is responsible for zeroizing the returned key after use
256fn derive_key(passphrase: &str, salt: &[u8; SALT_LEN]) -> Result<[u8; 32], StoreError> {
257    let params = Params::new(
258        ARGON2_MEMORY_KIB,
259        ARGON2_ITERATIONS,
260        ARGON2_PARALLELISM,
261        Some(ARGON2_OUTPUT_LEN),
262    )
263    .map_err(|_| StoreError::EncryptionFailed)?;
264
265    let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
266
267    let mut output = [0u8; 32];
268    argon2
269        .hash_password_into(passphrase.as_bytes(), salt, &mut output)
270        .map_err(|_| StoreError::EncryptionFailed)?;
271
272    Ok(output)
273}
274
275// ============================================================================
276// Encryption / Decryption
277// ============================================================================
278
279/// Encrypt a secret key with a passphrase.
280///
281/// # Arguments
282///
283/// * `secret_key` - The secret key to encrypt
284/// * `passphrase` - The passphrase to use for key derivation
285///
286/// # Returns
287///
288/// An `EncryptedKey` containing the encrypted key material and all data
289/// needed for decryption (salt, nonce).
290///
291/// # Errors
292///
293/// Returns `StoreError::EncryptionFailed` if:
294/// - Key derivation fails
295/// - Encryption fails (should not happen with valid inputs)
296///
297/// # Security
298///
299/// - Generates fresh random salt and nonce for each encryption
300/// - Uses cryptographically secure OS random number generator
301/// - Zeroizes the derived encryption key after use
302///
303/// # Example
304///
305/// ```rust
306/// use txgate_crypto::keys::SecretKey;
307/// use txgate_crypto::encryption::encrypt_key;
308///
309/// let secret_key = SecretKey::generate();
310/// let encrypted = encrypt_key(&secret_key, "my passphrase").expect("encryption failed");
311///
312/// // The encrypted data can be serialized and stored
313/// let bytes = encrypted.to_bytes();
314/// ```
315pub fn encrypt_key(secret_key: &SecretKey, passphrase: &str) -> Result<EncryptedKey, StoreError> {
316    // 1. Generate random salt and nonce using OS RNG
317    let mut salt = [0u8; SALT_LEN];
318    let mut nonce_bytes = [0u8; NONCE_LEN];
319    rand::rngs::OsRng.fill_bytes(&mut salt);
320    rand::rngs::OsRng.fill_bytes(&mut nonce_bytes);
321
322    // 2. Derive encryption key
323    let mut encryption_key = derive_key(passphrase, &salt)?;
324
325    // 3. Encrypt with ChaCha20-Poly1305
326    let cipher = ChaCha20Poly1305::new_from_slice(&encryption_key)
327        .map_err(|_| StoreError::EncryptionFailed)?;
328    let nonce = Nonce::from_slice(&nonce_bytes);
329
330    let ciphertext = cipher
331        .encrypt(nonce, secret_key.as_bytes().as_ref())
332        .map_err(|_| StoreError::EncryptionFailed)?;
333
334    // 4. Zeroize the encryption key immediately
335    encryption_key.zeroize();
336
337    Ok(EncryptedKey {
338        version: ENCRYPTION_VERSION,
339        salt,
340        nonce: nonce_bytes,
341        ciphertext,
342    })
343}
344
345/// Decrypt a secret key with a passphrase.
346///
347/// # Arguments
348///
349/// * `encrypted` - The encrypted key container
350/// * `passphrase` - The passphrase used during encryption
351///
352/// # Returns
353///
354/// The decrypted `SecretKey`.
355///
356/// # Errors
357///
358/// Returns `StoreError::InvalidFormat` if:
359/// - The version byte is not recognized
360/// - The ciphertext length is invalid
361///
362/// Returns `StoreError::DecryptionFailed` if:
363/// - The passphrase is incorrect
364/// - The ciphertext has been tampered with
365/// - The authentication tag verification fails
366///
367/// # Security
368///
369/// - ChaCha20-Poly1305 provides authenticated decryption, so any tampering
370///   with the ciphertext will be detected
371/// - Error messages are intentionally generic to avoid leaking information
372/// - The derived encryption key is zeroized after use
373///
374/// # Example
375///
376/// ```rust
377/// use txgate_crypto::keys::SecretKey;
378/// use txgate_crypto::encryption::{encrypt_key, decrypt_key};
379///
380/// let original = SecretKey::generate();
381/// let encrypted = encrypt_key(&original, "passphrase").expect("encryption failed");
382///
383/// let decrypted = decrypt_key(&encrypted, "passphrase").expect("decryption failed");
384/// assert_eq!(original.as_bytes(), decrypted.as_bytes());
385/// ```
386pub fn decrypt_key(encrypted: &EncryptedKey, passphrase: &str) -> Result<SecretKey, StoreError> {
387    // 1. Check version
388    if encrypted.version != ENCRYPTION_VERSION {
389        return Err(StoreError::InvalidFormat);
390    }
391
392    // 2. Validate ciphertext length (should be PLAINTEXT_LEN + TAG_LEN)
393    let expected_ciphertext_len = PLAINTEXT_LEN + TAG_LEN;
394    if encrypted.ciphertext.len() != expected_ciphertext_len {
395        return Err(StoreError::InvalidFormat);
396    }
397
398    // 3. Derive encryption key
399    let mut encryption_key = derive_key(passphrase, &encrypted.salt)?;
400
401    // 4. Decrypt with ChaCha20-Poly1305
402    let cipher = ChaCha20Poly1305::new_from_slice(&encryption_key)
403        .map_err(|_| StoreError::DecryptionFailed)?;
404    let nonce = Nonce::from_slice(&encrypted.nonce);
405
406    // Use Zeroizing wrapper to ensure automatic cleanup on drop
407    let plaintext = Zeroizing::new(
408        cipher
409            .decrypt(nonce, encrypted.ciphertext.as_ref())
410            .map_err(|_| StoreError::DecryptionFailed)?,
411    );
412
413    // 5. Zeroize the encryption key immediately
414    encryption_key.zeroize();
415
416    // 6. Convert to SecretKey (plaintext is automatically zeroized when dropped)
417    plaintext
418        .as_slice()
419        .try_into()
420        .map_err(|_| StoreError::InvalidFormat)
421        .map(SecretKey::new)
422}
423
424// ============================================================================
425// Tests
426// ============================================================================
427
428#[cfg(test)]
429mod tests {
430    #![allow(clippy::expect_used)]
431    #![allow(clippy::indexing_slicing)]
432
433    use super::*;
434
435    #[test]
436    fn test_encrypt_decrypt_round_trip() {
437        let original = SecretKey::generate();
438        let passphrase = "test passphrase 123!";
439
440        let encrypted = encrypt_key(&original, passphrase).expect("encryption should succeed");
441        let decrypted = decrypt_key(&encrypted, passphrase).expect("decryption should succeed");
442
443        assert_eq!(original.as_bytes(), decrypted.as_bytes());
444    }
445
446    #[test]
447    fn test_different_passphrases_produce_different_ciphertexts() {
448        let secret_key = SecretKey::new([0x42u8; 32]);
449
450        let encrypted1 =
451            encrypt_key(&secret_key, "passphrase1").expect("encryption should succeed");
452        let encrypted2 =
453            encrypt_key(&secret_key, "passphrase2").expect("encryption should succeed");
454
455        // Different passphrases should produce different derived keys, thus different ciphertexts
456        assert_ne!(encrypted1.ciphertext, encrypted2.ciphertext);
457    }
458
459    #[test]
460    fn test_same_passphrase_different_salts() {
461        let secret_key = SecretKey::new([0x42u8; 32]);
462        let passphrase = "same passphrase";
463
464        let encrypted1 = encrypt_key(&secret_key, passphrase).expect("encryption should succeed");
465        let encrypted2 = encrypt_key(&secret_key, passphrase).expect("encryption should succeed");
466
467        // Even with the same passphrase, random salts should produce different ciphertexts
468        assert_ne!(encrypted1.salt, encrypted2.salt);
469        assert_ne!(encrypted1.nonce, encrypted2.nonce);
470        assert_ne!(encrypted1.ciphertext, encrypted2.ciphertext);
471    }
472
473    #[test]
474    fn test_wrong_passphrase_fails_decryption() {
475        let secret_key = SecretKey::generate();
476        let correct_passphrase = "correct passphrase";
477        let wrong_passphrase = "wrong passphrase";
478
479        let encrypted =
480            encrypt_key(&secret_key, correct_passphrase).expect("encryption should succeed");
481        let result = decrypt_key(&encrypted, wrong_passphrase);
482
483        assert!(result.is_err());
484        assert!(matches!(result, Err(StoreError::DecryptionFailed)));
485    }
486
487    #[test]
488    fn test_serialization_round_trip() {
489        let secret_key = SecretKey::generate();
490        let passphrase = "test passphrase";
491
492        let encrypted = encrypt_key(&secret_key, passphrase).expect("encryption should succeed");
493        let bytes = encrypted.to_bytes();
494
495        assert_eq!(bytes.len(), ENCRYPTED_KEY_LEN);
496
497        let deserialized =
498            EncryptedKey::from_bytes(&bytes).expect("deserialization should succeed");
499        let decrypted = decrypt_key(&deserialized, passphrase).expect("decryption should succeed");
500
501        assert_eq!(secret_key.as_bytes(), decrypted.as_bytes());
502    }
503
504    #[test]
505    fn test_invalid_format_wrong_length() {
506        let too_short = vec![0u8; 10];
507        let result = EncryptedKey::from_bytes(&too_short);
508        assert!(result.is_err());
509        assert!(matches!(result, Err(StoreError::InvalidFormat)));
510
511        let too_long = vec![0u8; 100];
512        let result = EncryptedKey::from_bytes(&too_long);
513        assert!(result.is_err());
514        assert!(matches!(result, Err(StoreError::InvalidFormat)));
515    }
516
517    #[test]
518    fn test_invalid_format_wrong_version() {
519        let mut bytes = vec![0u8; ENCRYPTED_KEY_LEN];
520        bytes[0] = 99; // Invalid version
521
522        let result = EncryptedKey::from_bytes(&bytes);
523        assert!(result.is_err());
524        assert!(matches!(result, Err(StoreError::InvalidFormat)));
525    }
526
527    #[test]
528    fn test_tampered_ciphertext_fails_decryption() {
529        let secret_key = SecretKey::generate();
530        let passphrase = "test passphrase";
531
532        let mut encrypted =
533            encrypt_key(&secret_key, passphrase).expect("encryption should succeed");
534
535        // Tamper with a byte in the ciphertext
536        if let Some(byte) = encrypted.ciphertext.first_mut() {
537            *byte ^= 0xFF;
538        }
539
540        let result = decrypt_key(&encrypted, passphrase);
541        assert!(result.is_err());
542        assert!(matches!(result, Err(StoreError::DecryptionFailed)));
543    }
544
545    #[test]
546    fn test_tampered_tag_fails_decryption() {
547        let secret_key = SecretKey::generate();
548        let passphrase = "test passphrase";
549
550        let mut encrypted =
551            encrypt_key(&secret_key, passphrase).expect("encryption should succeed");
552
553        // Tamper with the last byte (part of the auth tag)
554        if let Some(byte) = encrypted.ciphertext.last_mut() {
555            *byte ^= 0xFF;
556        }
557
558        let result = decrypt_key(&encrypted, passphrase);
559        assert!(result.is_err());
560        assert!(matches!(result, Err(StoreError::DecryptionFailed)));
561    }
562
563    #[test]
564    fn test_tampered_nonce_fails_decryption() {
565        let secret_key = SecretKey::generate();
566        let passphrase = "test passphrase";
567
568        let mut encrypted =
569            encrypt_key(&secret_key, passphrase).expect("encryption should succeed");
570
571        // Tamper with the nonce
572        encrypted.nonce[0] ^= 0xFF;
573
574        let result = decrypt_key(&encrypted, passphrase);
575        assert!(result.is_err());
576        assert!(matches!(result, Err(StoreError::DecryptionFailed)));
577    }
578
579    #[test]
580    fn test_tampered_salt_fails_decryption() {
581        let secret_key = SecretKey::generate();
582        let passphrase = "test passphrase";
583
584        let mut encrypted =
585            encrypt_key(&secret_key, passphrase).expect("encryption should succeed");
586
587        // Tamper with the salt (will derive a different key)
588        encrypted.salt[0] ^= 0xFF;
589
590        let result = decrypt_key(&encrypted, passphrase);
591        assert!(result.is_err());
592        assert!(matches!(result, Err(StoreError::DecryptionFailed)));
593    }
594
595    #[test]
596    fn test_encrypted_key_to_bytes_format() {
597        let secret_key = SecretKey::generate();
598        let passphrase = "test passphrase";
599
600        let encrypted = encrypt_key(&secret_key, passphrase).expect("encryption should succeed");
601        let bytes = encrypted.to_bytes();
602
603        // Verify format
604        assert_eq!(bytes.len(), 77);
605        assert_eq!(bytes[0], ENCRYPTION_VERSION);
606        assert_eq!(&bytes[1..17], &encrypted.salt);
607        assert_eq!(&bytes[17..29], &encrypted.nonce);
608        assert_eq!(&bytes[29..], &encrypted.ciphertext[..]);
609    }
610
611    #[test]
612    fn test_empty_passphrase() {
613        let secret_key = SecretKey::generate();
614        let passphrase = "";
615
616        // Empty passphrase should still work (though not recommended)
617        let encrypted = encrypt_key(&secret_key, passphrase).expect("encryption should succeed");
618        let decrypted = decrypt_key(&encrypted, passphrase).expect("decryption should succeed");
619
620        assert_eq!(secret_key.as_bytes(), decrypted.as_bytes());
621    }
622
623    #[test]
624    fn test_unicode_passphrase() {
625        let secret_key = SecretKey::generate();
626        let passphrase = "test passphrase with unicode: \u{1F512}";
627
628        let encrypted = encrypt_key(&secret_key, passphrase).expect("encryption should succeed");
629        let decrypted = decrypt_key(&encrypted, passphrase).expect("decryption should succeed");
630
631        assert_eq!(secret_key.as_bytes(), decrypted.as_bytes());
632    }
633
634    #[test]
635    fn test_long_passphrase() {
636        let secret_key = SecretKey::generate();
637        let passphrase = "a".repeat(10000);
638
639        let encrypted = encrypt_key(&secret_key, &passphrase).expect("encryption should succeed");
640        let decrypted = decrypt_key(&encrypted, &passphrase).expect("decryption should succeed");
641
642        assert_eq!(secret_key.as_bytes(), decrypted.as_bytes());
643    }
644
645    #[test]
646    fn test_version_check_on_decrypt() {
647        let encrypted = EncryptedKey {
648            version: 2, // Unsupported version
649            salt: [0u8; SALT_LEN],
650            nonce: [0u8; NONCE_LEN],
651            ciphertext: vec![0u8; PLAINTEXT_LEN + TAG_LEN],
652        };
653
654        let result = decrypt_key(&encrypted, "passphrase");
655        assert!(result.is_err());
656        assert!(matches!(result, Err(StoreError::InvalidFormat)));
657    }
658
659    #[test]
660    fn test_ciphertext_length_validation() {
661        let encrypted = EncryptedKey {
662            version: ENCRYPTION_VERSION,
663            salt: [0u8; SALT_LEN],
664            nonce: [0u8; NONCE_LEN],
665            ciphertext: vec![0u8; 10], // Too short
666        };
667
668        let result = decrypt_key(&encrypted, "passphrase");
669        assert!(result.is_err());
670        assert!(matches!(result, Err(StoreError::InvalidFormat)));
671    }
672
673    #[test]
674    fn test_encrypted_key_debug() {
675        let secret_key = SecretKey::generate();
676        let encrypted = encrypt_key(&secret_key, "passphrase").expect("encryption should succeed");
677
678        // Debug should not panic and should produce some output
679        let debug_output = format!("{encrypted:?}");
680        assert!(debug_output.contains("EncryptedKey"));
681    }
682
683    #[test]
684    fn test_encrypted_key_clone() {
685        let secret_key = SecretKey::generate();
686        let encrypted = encrypt_key(&secret_key, "passphrase").expect("encryption should succeed");
687
688        let cloned = encrypted.clone();
689        assert_eq!(encrypted.version, cloned.version);
690        assert_eq!(encrypted.salt, cloned.salt);
691        assert_eq!(encrypted.nonce, cloned.nonce);
692        assert_eq!(encrypted.ciphertext, cloned.ciphertext);
693    }
694
695    // Test Send + Sync traits
696    #[test]
697    fn test_encrypted_key_is_send_sync() {
698        fn assert_send_sync<T: Send + Sync>() {}
699        assert_send_sync::<EncryptedKey>();
700    }
701
702    // Test that encryption constants are correct
703    #[test]
704    fn test_encryption_constants() {
705        assert_eq!(ENCRYPTION_VERSION, 1);
706        assert_eq!(SALT_LEN, 16);
707        assert_eq!(NONCE_LEN, 12);
708        assert_eq!(TAG_LEN, 16);
709        assert_eq!(PLAINTEXT_LEN, 32);
710        assert_eq!(ENCRYPTED_KEY_LEN, 77);
711    }
712
713    // ------------------------------------------------------------------------
714    // Key Derivation Tests
715    // ------------------------------------------------------------------------
716
717    #[test]
718    fn test_derive_key_deterministic() {
719        // Same passphrase and salt should produce the same key
720        let passphrase = "test passphrase";
721        let salt = [0x42u8; SALT_LEN];
722
723        let key1 = derive_key(passphrase, &salt).expect("derivation should succeed");
724        let key2 = derive_key(passphrase, &salt).expect("derivation should succeed");
725
726        assert_eq!(key1, key2);
727    }
728
729    #[test]
730    fn test_derive_key_different_salts() {
731        let passphrase = "test passphrase";
732        let salt1 = [0x42u8; SALT_LEN];
733        let salt2 = [0x43u8; SALT_LEN];
734
735        let key1 = derive_key(passphrase, &salt1).expect("derivation should succeed");
736        let key2 = derive_key(passphrase, &salt2).expect("derivation should succeed");
737
738        // Different salts should produce different keys
739        assert_ne!(key1, key2);
740    }
741
742    #[test]
743    fn test_derive_key_different_passphrases() {
744        let salt = [0x42u8; SALT_LEN];
745
746        let key1 = derive_key("passphrase1", &salt).expect("derivation should succeed");
747        let key2 = derive_key("passphrase2", &salt).expect("derivation should succeed");
748
749        // Different passphrases should produce different keys
750        assert_ne!(key1, key2);
751    }
752
753    #[test]
754    fn test_derive_key_empty_passphrase() {
755        let salt = [0x42u8; SALT_LEN];
756
757        // Empty passphrase should still work (though not recommended)
758        let result = derive_key("", &salt);
759        assert!(result.is_ok());
760    }
761
762    #[test]
763    fn test_derive_key_output_length() {
764        let passphrase = "test";
765        let salt = [0x00u8; SALT_LEN];
766
767        let key = derive_key(passphrase, &salt).expect("derivation should succeed");
768
769        // Output should always be 32 bytes
770        assert_eq!(key.len(), 32);
771    }
772
773    // ------------------------------------------------------------------------
774    // Additional Edge Case Tests
775    // ------------------------------------------------------------------------
776
777    #[test]
778    fn test_encrypted_key_from_bytes_empty() {
779        let result = EncryptedKey::from_bytes(&[]);
780        assert!(matches!(result, Err(StoreError::InvalidFormat)));
781    }
782
783    #[test]
784    fn test_encrypted_key_version_field() {
785        let secret_key = SecretKey::generate();
786        let encrypted = encrypt_key(&secret_key, "test").expect("encryption should succeed");
787
788        assert_eq!(encrypted.version, ENCRYPTION_VERSION);
789    }
790
791    #[test]
792    fn test_decrypt_with_invalid_ciphertext_length() {
793        let encrypted = EncryptedKey {
794            version: ENCRYPTION_VERSION,
795            salt: [0u8; SALT_LEN],
796            nonce: [0u8; NONCE_LEN],
797            ciphertext: vec![0u8; 10], // Wrong length (should be 48)
798        };
799
800        let result = decrypt_key(&encrypted, "passphrase");
801        assert!(matches!(result, Err(StoreError::InvalidFormat)));
802    }
803
804    #[test]
805    fn test_decrypt_with_valid_length_but_wrong_data() {
806        let encrypted = EncryptedKey {
807            version: ENCRYPTION_VERSION,
808            salt: [0u8; SALT_LEN],
809            nonce: [0u8; NONCE_LEN],
810            ciphertext: vec![0u8; PLAINTEXT_LEN + TAG_LEN], // Correct length but garbage data
811        };
812
813        let result = decrypt_key(&encrypted, "passphrase");
814        assert!(matches!(result, Err(StoreError::DecryptionFailed)));
815    }
816}