Skip to main content

torsh_package/
security.rs

1//! Security features for package signing and encryption
2//!
3//! This module provides cryptographic security features including:
4//! - Digital signatures for package integrity and authenticity
5//! - Encryption for sensitive model packages
6//! - Key management and verification
7
8use std::collections::HashMap;
9use std::fs;
10use std::path::Path;
11
12use aes_gcm::aead::{Aead, KeyInit};
13use aes_gcm::{Aes256Gcm, Nonce as AesGcmNonce};
14use chacha20poly1305::{ChaCha20Poly1305, Nonce as ChaChaNonce};
15use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
16use serde::{Deserialize, Serialize};
17use sha2::{Digest, Sha256};
18use torsh_core::error::{Result, TorshError};
19
20use crate::package::Package;
21
22/// PBKDF2-HMAC-SHA256 iteration count (preserved from the prior ring impl).
23const PBKDF2_ITERATIONS: u32 = 100_000;
24
25/// AES-256-GCM nonce length in bytes (96-bit nonce).
26const AES_GCM_NONCE_LEN: usize = 12;
27
28/// ChaCha20-Poly1305 nonce length in bytes (96-bit nonce).
29const CHACHA20_NONCE_LEN: usize = 12;
30
31/// Package signature information
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct PackageSignature {
34    /// Signature algorithm used
35    pub algorithm: SignatureAlgorithm,
36    /// Signature data
37    pub signature: Vec<u8>,
38    /// Public key for verification
39    pub public_key: Vec<u8>,
40    /// Timestamp when signature was created
41    pub timestamp: chrono::DateTime<chrono::Utc>,
42    /// Additional signature metadata
43    pub metadata: HashMap<String, String>,
44}
45
46/// Supported signature algorithms
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
48pub enum SignatureAlgorithm {
49    /// Ed25519 digital signature
50    Ed25519,
51    /// RSA signature (future support)
52    Rsa,
53    /// ECDSA signature (future support)
54    Ecdsa,
55}
56
57/// Encryption algorithm
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
59pub enum EncryptionAlgorithm {
60    /// AES-256-GCM authenticated encryption
61    Aes256Gcm,
62    /// ChaCha20-Poly1305 authenticated encryption
63    ChaCha20Poly1305,
64}
65
66/// Encrypted package container
67#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct EncryptedPackage {
69    /// Encryption algorithm used
70    pub algorithm: EncryptionAlgorithm,
71    /// Encrypted package data
72    pub encrypted_data: Vec<u8>,
73    /// Nonce/IV for decryption
74    pub nonce: Vec<u8>,
75    /// Additional authenticated data (AAD)
76    pub aad: Option<Vec<u8>>,
77    /// Key derivation metadata
78    pub key_metadata: HashMap<String, String>,
79    /// Timestamp of encryption
80    pub timestamp: chrono::DateTime<chrono::Utc>,
81}
82
83/// Package signer for creating and verifying signatures
84pub struct PackageSigner {
85    signing_key: Option<SigningKey>,
86    verifying_keys: Vec<VerifyingKey>,
87    algorithm: SignatureAlgorithm,
88}
89
90/// Package encryptor for encrypting and decrypting packages
91pub struct PackageEncryptor {
92    algorithm: EncryptionAlgorithm,
93}
94
95/// Security error types
96#[derive(Debug, thiserror::Error)]
97pub enum SecurityError {
98    /// Signature verification failed
99    #[error("Signature verification failed: {0}")]
100    InvalidSignature(String),
101    /// Encryption/decryption error
102    #[error("Encryption error: {0}")]
103    EncryptionError(String),
104    /// Key management error
105    #[error("Key error: {0}")]
106    KeyError(String),
107}
108
109impl PackageSigner {
110    /// Create a new package signer with generated key pair
111    ///
112    /// # Panics
113    ///
114    /// Panics if the operating system CSPRNG is unavailable. Generating a
115    /// signing key from non-random bytes would be a critical security defect,
116    /// so failure to obtain entropy is treated as unrecoverable.
117    pub fn new() -> Self {
118        // Generate cryptographically secure random bytes for the signing key
119        // using the OS CSPRNG (getrandom — Pure Rust, replaces ring::rand).
120        let mut secret_bytes = [0u8; 32];
121        if let Err(err) = getrandom::fill(&mut secret_bytes) {
122            panic!("Failed to obtain entropy for signing key generation: {err}");
123        }
124
125        let signing_key = SigningKey::from_bytes(&secret_bytes);
126
127        Self {
128            signing_key: Some(signing_key),
129            verifying_keys: Vec::new(),
130            algorithm: SignatureAlgorithm::Ed25519,
131        }
132    }
133
134    /// Create a signer from an existing signing key
135    pub fn from_signing_key(signing_key: SigningKey) -> Self {
136        Self {
137            signing_key: Some(signing_key),
138            verifying_keys: Vec::new(),
139            algorithm: SignatureAlgorithm::Ed25519,
140        }
141    }
142
143    /// Create a verifier (no signing capability)
144    pub fn verifier_only() -> Self {
145        Self {
146            signing_key: None,
147            verifying_keys: Vec::new(),
148            algorithm: SignatureAlgorithm::Ed25519,
149        }
150    }
151
152    /// Add a trusted public key for verification
153    pub fn add_trusted_key(&mut self, public_key: &[u8]) -> Result<()> {
154        let verifying_key =
155            VerifyingKey::from_bytes(public_key.try_into().map_err(|_| {
156                TorshError::InvalidArgument("Invalid public key length".to_string())
157            })?)
158            .map_err(|e| TorshError::InvalidArgument(format!("Invalid public key: {}", e)))?;
159
160        self.verifying_keys.push(verifying_key);
161        Ok(())
162    }
163
164    /// Get public key for verification
165    pub fn public_key(&self) -> Option<Vec<u8>> {
166        self.signing_key
167            .as_ref()
168            .map(|sk| sk.verifying_key().to_bytes().to_vec())
169    }
170
171    /// Export signing key (use with caution!)
172    pub fn export_signing_key(&self) -> Option<Vec<u8>> {
173        self.signing_key.as_ref().map(|sk| sk.to_bytes().to_vec())
174    }
175
176    /// Sign a package
177    pub fn sign_package(&self, package: &Package) -> Result<PackageSignature> {
178        let signing_key = self
179            .signing_key
180            .as_ref()
181            .ok_or_else(|| TorshError::InvalidArgument("No signing key available".to_string()))?;
182
183        // Serialize package for signing
184        let package_data = self.package_digest(package)?;
185
186        // Create signature
187        let signature = signing_key.sign(&package_data);
188
189        let mut metadata = HashMap::new();
190        metadata.insert("package_name".to_string(), package.name().to_string());
191        metadata.insert(
192            "package_version".to_string(),
193            package.get_version().to_string(),
194        );
195
196        Ok(PackageSignature {
197            algorithm: self.algorithm,
198            signature: signature.to_bytes().to_vec(),
199            public_key: signing_key.verifying_key().to_bytes().to_vec(),
200            timestamp: chrono::Utc::now(),
201            metadata,
202        })
203    }
204
205    /// Verify package signature
206    pub fn verify_package(&self, package: &Package, signature: &PackageSignature) -> Result<bool> {
207        if signature.algorithm != self.algorithm {
208            return Err(TorshError::InvalidArgument(format!(
209                "Unsupported signature algorithm: {:?}",
210                signature.algorithm
211            )));
212        }
213
214        // Get verifying key from signature
215        let verifying_key =
216            VerifyingKey::from_bytes(signature.public_key.as_slice().try_into().map_err(|_| {
217                TorshError::InvalidArgument("Invalid public key length".to_string())
218            })?)
219            .map_err(|e| TorshError::InvalidArgument(format!("Invalid public key: {}", e)))?;
220
221        // Check if the key is trusted
222        if !self.verifying_keys.is_empty()
223            && !self
224                .verifying_keys
225                .iter()
226                .any(|k| k.to_bytes() == verifying_key.to_bytes())
227        {
228            return Ok(false);
229        }
230
231        // Compute package digest
232        let package_data = self.package_digest(package)?;
233
234        // Parse signature
235        let sig =
236            Signature::from_bytes(signature.signature.as_slice().try_into().map_err(|_| {
237                TorshError::InvalidArgument("Invalid signature length".to_string())
238            })?);
239
240        // Verify signature
241        match verifying_key.verify(&package_data, &sig) {
242            Ok(()) => Ok(true),
243            Err(_) => Ok(false),
244        }
245    }
246
247    /// Compute package digest for signing
248    fn package_digest(&self, package: &Package) -> Result<Vec<u8>> {
249        // Create a deterministic digest of the package
250        let mut hasher = Sha256::new();
251
252        // Hash package metadata
253        hasher.update(package.name().as_bytes());
254        hasher.update(package.get_version().as_bytes());
255
256        // Hash resources in sorted order for determinism
257        let mut resource_names: Vec<_> = package.resources().keys().collect();
258        resource_names.sort();
259
260        for name in resource_names {
261            if let Some(resource) = package.resources().get(name) {
262                hasher.update(name.as_bytes());
263                hasher.update(&resource.data);
264            }
265        }
266
267        Ok(hasher.finalize().to_vec())
268    }
269
270    /// Save signature to file
271    pub fn save_signature<P: AsRef<Path>>(signature: &PackageSignature, path: P) -> Result<()> {
272        let serialized = oxicode::serde::encode_to_vec(signature, oxicode::config::standard())
273            .map_err(|e| TorshError::SerializationError(e.to_string()))?;
274
275        fs::write(path, serialized).map_err(|e| TorshError::IoError(e.to_string()))?;
276        Ok(())
277    }
278
279    /// Load signature from file
280    pub fn load_signature<P: AsRef<Path>>(path: P) -> Result<PackageSignature> {
281        let data = fs::read(path).map_err(|e| TorshError::IoError(e.to_string()))?;
282
283        let (signature, _) = oxicode::serde::decode_from_slice(&data, oxicode::config::standard())
284            .map_err(|e| TorshError::SerializationError(e.to_string()))?;
285
286        Ok(signature)
287    }
288}
289
290impl Default for PackageSigner {
291    fn default() -> Self {
292        Self::new()
293    }
294}
295
296impl PackageEncryptor {
297    /// Create a new package encryptor
298    pub fn new(algorithm: EncryptionAlgorithm) -> Self {
299        Self { algorithm }
300    }
301
302    /// Encrypt a package with a password
303    pub fn encrypt_package_with_password(
304        &self,
305        package: &Package,
306        password: &str,
307    ) -> Result<EncryptedPackage> {
308        // Serialize package
309        let package_data = oxicode::serde::encode_to_vec(package, oxicode::config::standard())
310            .map_err(|e| TorshError::SerializationError(e.to_string()))?;
311
312        // Derive key from password using PBKDF2
313        let salt = self.generate_salt()?;
314        let key = self.derive_key_from_password(password, &salt)?;
315
316        // Encrypt data
317        let (encrypted_data, nonce) = self.encrypt_data(&package_data, &key)?;
318
319        let mut key_metadata = HashMap::new();
320        key_metadata.insert("kdf".to_string(), "pbkdf2".to_string());
321        key_metadata.insert("salt".to_string(), hex::encode(&salt));
322        key_metadata.insert("iterations".to_string(), "100000".to_string());
323
324        Ok(EncryptedPackage {
325            algorithm: self.algorithm,
326            encrypted_data,
327            nonce,
328            aad: None,
329            key_metadata,
330            timestamp: chrono::Utc::now(),
331        })
332    }
333
334    /// Decrypt a package with a password
335    pub fn decrypt_package_with_password(
336        &self,
337        encrypted: &EncryptedPackage,
338        password: &str,
339    ) -> Result<Package> {
340        // Extract salt from metadata
341        let salt_hex = encrypted
342            .key_metadata
343            .get("salt")
344            .ok_or_else(|| TorshError::InvalidArgument("Missing salt in metadata".to_string()))?;
345
346        let salt = hex::decode(salt_hex)
347            .map_err(|e| TorshError::InvalidArgument(format!("Invalid salt: {}", e)))?;
348
349        // Derive key from password
350        let key = self.derive_key_from_password(password, &salt)?;
351
352        // Decrypt data
353        let decrypted_data =
354            self.decrypt_data(&encrypted.encrypted_data, &encrypted.nonce, &key)?;
355
356        // Deserialize package
357        let (package, _) =
358            oxicode::serde::decode_from_slice(&decrypted_data, oxicode::config::standard())
359                .map_err(|e| TorshError::SerializationError(e.to_string()))?;
360
361        Ok(package)
362    }
363
364    /// Encrypt data with AES-256-GCM
365    fn encrypt_data(&self, data: &[u8], key: &[u8]) -> Result<(Vec<u8>, Vec<u8>)> {
366        match self.algorithm {
367            EncryptionAlgorithm::Aes256Gcm => self.encrypt_aes_gcm(data, key),
368            EncryptionAlgorithm::ChaCha20Poly1305 => self.encrypt_chacha20(data, key),
369        }
370    }
371
372    /// Decrypt data
373    fn decrypt_data(&self, encrypted: &[u8], nonce: &[u8], key: &[u8]) -> Result<Vec<u8>> {
374        match self.algorithm {
375            EncryptionAlgorithm::Aes256Gcm => self.decrypt_aes_gcm(encrypted, nonce, key),
376            EncryptionAlgorithm::ChaCha20Poly1305 => self.decrypt_chacha20(encrypted, nonce, key),
377        }
378    }
379
380    /// Encrypt with AES-256-GCM
381    ///
382    /// Produces `ciphertext || 16-byte authentication tag` with empty AAD,
383    /// matching the byte layout previously produced via ring's
384    /// `seal_in_place_append_tag`. The 96-bit nonce is returned separately.
385    fn encrypt_aes_gcm(&self, data: &[u8], key: &[u8]) -> Result<(Vec<u8>, Vec<u8>)> {
386        let cipher = Aes256Gcm::new_from_slice(key)
387            .map_err(|_| TorshError::InvalidArgument("Invalid key for AES-256-GCM".to_string()))?;
388
389        let nonce_bytes = self.generate_nonce(AES_GCM_NONCE_LEN)?;
390        let nonce = AesGcmNonce::try_from(nonce_bytes.as_slice())
391            .map_err(|_| TorshError::InvalidArgument("Invalid nonce".to_string()))?;
392
393        let ciphertext = cipher
394            .encrypt(&nonce, data)
395            .map_err(|_| TorshError::InvalidArgument("Encryption failed".to_string()))?;
396
397        Ok((ciphertext, nonce_bytes))
398    }
399
400    /// Decrypt with AES-256-GCM
401    fn decrypt_aes_gcm(&self, encrypted: &[u8], nonce: &[u8], key: &[u8]) -> Result<Vec<u8>> {
402        let cipher = Aes256Gcm::new_from_slice(key)
403            .map_err(|_| TorshError::InvalidArgument("Invalid key for AES-256-GCM".to_string()))?;
404
405        let nonce = AesGcmNonce::try_from(nonce)
406            .map_err(|_| TorshError::InvalidArgument("Invalid nonce".to_string()))?;
407
408        cipher
409            .decrypt(&nonce, encrypted)
410            .map_err(|_| TorshError::InvalidArgument("Decryption failed".to_string()))
411    }
412
413    /// Encrypt with ChaCha20-Poly1305
414    ///
415    /// Produces `ciphertext || 16-byte authentication tag` with empty AAD,
416    /// matching the byte layout previously produced via ring's
417    /// `seal_in_place_append_tag`. The 96-bit nonce is returned separately.
418    fn encrypt_chacha20(&self, data: &[u8], key: &[u8]) -> Result<(Vec<u8>, Vec<u8>)> {
419        let cipher = ChaCha20Poly1305::new_from_slice(key).map_err(|_| {
420            TorshError::InvalidArgument("Invalid key for ChaCha20-Poly1305".to_string())
421        })?;
422
423        let nonce_bytes = self.generate_nonce(CHACHA20_NONCE_LEN)?;
424        let nonce = ChaChaNonce::try_from(nonce_bytes.as_slice())
425            .map_err(|_| TorshError::InvalidArgument("Invalid nonce".to_string()))?;
426
427        let ciphertext = cipher
428            .encrypt(&nonce, data)
429            .map_err(|_| TorshError::InvalidArgument("Encryption failed".to_string()))?;
430
431        Ok((ciphertext, nonce_bytes))
432    }
433
434    /// Decrypt with ChaCha20-Poly1305
435    fn decrypt_chacha20(&self, encrypted: &[u8], nonce: &[u8], key: &[u8]) -> Result<Vec<u8>> {
436        let cipher = ChaCha20Poly1305::new_from_slice(key).map_err(|_| {
437            TorshError::InvalidArgument("Invalid key for ChaCha20-Poly1305".to_string())
438        })?;
439
440        let nonce = ChaChaNonce::try_from(nonce)
441            .map_err(|_| TorshError::InvalidArgument("Invalid nonce".to_string()))?;
442
443        cipher
444            .decrypt(&nonce, encrypted)
445            .map_err(|_| TorshError::InvalidArgument("Decryption failed".to_string()))
446    }
447
448    /// Derive encryption key from password using PBKDF2-HMAC-SHA256
449    ///
450    /// Uses 100_000 iterations and produces a 256-bit (32-byte) key. These
451    /// parameters are preserved exactly from the prior `ring::pbkdf2`
452    /// implementation so previously encrypted packages still decrypt.
453    fn derive_key_from_password(&self, password: &str, salt: &[u8]) -> Result<Vec<u8>> {
454        use pbkdf2::pbkdf2_hmac;
455
456        let mut key = vec![0u8; 32]; // 256-bit key
457
458        // `Sha256` is imported at module scope (also used for package digests).
459        pbkdf2_hmac::<Sha256>(password.as_bytes(), salt, PBKDF2_ITERATIONS, &mut key);
460
461        Ok(key)
462    }
463
464    /// Generate a cryptographically secure random salt (32 bytes)
465    fn generate_salt(&self) -> Result<Vec<u8>> {
466        let mut salt = vec![0u8; 32];
467        getrandom::fill(&mut salt)
468            .map_err(|e| TorshError::InvalidArgument(format!("Failed to generate salt: {e}")))?;
469        Ok(salt)
470    }
471
472    /// Generate a cryptographically secure random nonce of `len` bytes
473    fn generate_nonce(&self, len: usize) -> Result<Vec<u8>> {
474        let mut nonce = vec![0u8; len];
475        getrandom::fill(&mut nonce)
476            .map_err(|e| TorshError::InvalidArgument(format!("Failed to generate nonce: {e}")))?;
477        Ok(nonce)
478    }
479
480    /// Save encrypted package to file
481    pub fn save_encrypted<P: AsRef<Path>>(encrypted: &EncryptedPackage, path: P) -> Result<()> {
482        let serialized = oxicode::serde::encode_to_vec(encrypted, oxicode::config::standard())
483            .map_err(|e| TorshError::SerializationError(e.to_string()))?;
484
485        fs::write(path, serialized).map_err(|e| TorshError::IoError(e.to_string()))?;
486        Ok(())
487    }
488
489    /// Load encrypted package from file
490    pub fn load_encrypted<P: AsRef<Path>>(path: P) -> Result<EncryptedPackage> {
491        let data = fs::read(path).map_err(|e| TorshError::IoError(e.to_string()))?;
492
493        let (encrypted, _) = oxicode::serde::decode_from_slice(&data, oxicode::config::standard())
494            .map_err(|e| TorshError::SerializationError(e.to_string()))?;
495
496        Ok(encrypted)
497    }
498}
499
500#[cfg(test)]
501mod tests {
502    use super::*;
503
504    #[test]
505    fn test_package_signer_creation() {
506        let signer = PackageSigner::new();
507        assert!(signer.public_key().is_some());
508        assert!(signer.export_signing_key().is_some());
509    }
510
511    #[test]
512    fn test_sign_and_verify_package() {
513        let signer = PackageSigner::new();
514        let package = Package::new("test".to_string(), "1.0.0".to_string());
515
516        let signature = signer.sign_package(&package).unwrap();
517        assert_eq!(signature.algorithm, SignatureAlgorithm::Ed25519);
518
519        let is_valid = signer.verify_package(&package, &signature).unwrap();
520        assert!(is_valid);
521    }
522
523    #[test]
524    fn test_signature_fails_on_modified_package() {
525        let signer = PackageSigner::new();
526        let mut package = Package::new("test".to_string(), "1.0.0".to_string());
527
528        let signature = signer.sign_package(&package).unwrap();
529
530        // Modify package
531        package.add_source_file("new", "new content").unwrap();
532
533        let is_valid = signer.verify_package(&package, &signature).unwrap();
534        assert!(!is_valid);
535    }
536
537    #[test]
538    fn test_encrypt_decrypt_package() {
539        let encryptor = PackageEncryptor::new(EncryptionAlgorithm::Aes256Gcm);
540        let package = Package::new("secret".to_string(), "1.0.0".to_string());
541        let password = "super_secret_password";
542
543        let encrypted = encryptor
544            .encrypt_package_with_password(&package, password)
545            .unwrap();
546        assert_eq!(encrypted.algorithm, EncryptionAlgorithm::Aes256Gcm);
547
548        let decrypted = encryptor
549            .decrypt_package_with_password(&encrypted, password)
550            .unwrap();
551        assert_eq!(decrypted.name(), package.name());
552        assert_eq!(decrypted.get_version(), package.get_version());
553    }
554
555    #[test]
556    fn test_decrypt_with_wrong_password_fails() {
557        let encryptor = PackageEncryptor::new(EncryptionAlgorithm::Aes256Gcm);
558        let package = Package::new("secret".to_string(), "1.0.0".to_string());
559
560        let encrypted = encryptor
561            .encrypt_package_with_password(&package, "correct_password")
562            .unwrap();
563
564        let result = encryptor.decrypt_package_with_password(&encrypted, "wrong_password");
565        assert!(result.is_err());
566    }
567
568    #[test]
569    fn test_chacha20_encryption() {
570        let encryptor = PackageEncryptor::new(EncryptionAlgorithm::ChaCha20Poly1305);
571        let package = Package::new("test".to_string(), "1.0.0".to_string());
572        let password = "test_password";
573
574        let encrypted = encryptor
575            .encrypt_package_with_password(&package, password)
576            .unwrap();
577        assert_eq!(encrypted.algorithm, EncryptionAlgorithm::ChaCha20Poly1305);
578
579        let decrypted = encryptor
580            .decrypt_package_with_password(&encrypted, password)
581            .unwrap();
582        assert_eq!(decrypted.name(), package.name());
583    }
584
585    #[test]
586    fn test_trusted_key_verification() {
587        let signer = PackageSigner::new();
588        let mut verifier = PackageSigner::verifier_only();
589
590        // Add the signer's public key as trusted
591        verifier
592            .add_trusted_key(&signer.public_key().unwrap())
593            .unwrap();
594
595        let package = Package::new("test".to_string(), "1.0.0".to_string());
596        let signature = signer.sign_package(&package).unwrap();
597
598        let is_valid = verifier.verify_package(&package, &signature).unwrap();
599        assert!(is_valid);
600    }
601
602    #[test]
603    fn test_untrusted_key_verification_fails() {
604        let signer = PackageSigner::new();
605        let mut verifier = PackageSigner::verifier_only();
606
607        // Add a different public key as trusted
608        let other_signer = PackageSigner::new();
609        verifier
610            .add_trusted_key(&other_signer.public_key().unwrap())
611            .unwrap();
612
613        let package = Package::new("test".to_string(), "1.0.0".to_string());
614        let signature = signer.sign_package(&package).unwrap();
615
616        let is_valid = verifier.verify_package(&package, &signature).unwrap();
617        assert!(!is_valid);
618    }
619
620    #[test]
621    fn test_aes_gcm_roundtrip_and_format() {
622        let enc = PackageEncryptor::new(EncryptionAlgorithm::Aes256Gcm);
623        let key = [0x11u8; 32];
624        let plaintext = b"torsh aes-256-gcm payload";
625
626        let (ciphertext, nonce) = enc.encrypt_aes_gcm(plaintext, &key).unwrap();
627        // ring's seal_in_place_append_tag produced ciphertext || 16-byte tag;
628        // RustCrypto preserves the same layout.
629        assert_eq!(nonce.len(), AES_GCM_NONCE_LEN);
630        assert_eq!(ciphertext.len(), plaintext.len() + 16);
631
632        let decrypted = enc.decrypt_aes_gcm(&ciphertext, &nonce, &key).unwrap();
633        assert_eq!(decrypted, plaintext);
634    }
635
636    #[test]
637    fn test_chacha20_roundtrip_and_format() {
638        let enc = PackageEncryptor::new(EncryptionAlgorithm::ChaCha20Poly1305);
639        let key = [0x22u8; 32];
640        let plaintext = b"torsh chacha20-poly1305 payload";
641
642        let (ciphertext, nonce) = enc.encrypt_chacha20(plaintext, &key).unwrap();
643        assert_eq!(nonce.len(), CHACHA20_NONCE_LEN);
644        assert_eq!(ciphertext.len(), plaintext.len() + 16);
645
646        let decrypted = enc.decrypt_chacha20(&ciphertext, &nonce, &key).unwrap();
647        assert_eq!(decrypted, plaintext);
648    }
649
650    #[test]
651    fn test_aes_gcm_tamper_is_rejected() {
652        let enc = PackageEncryptor::new(EncryptionAlgorithm::Aes256Gcm);
653        let key = [0x33u8; 32];
654        let plaintext = b"authenticate me";
655
656        let (mut ciphertext, nonce) = enc.encrypt_aes_gcm(plaintext, &key).unwrap();
657        // Flip a single bit in the ciphertext: authentication MUST fail.
658        ciphertext[0] ^= 0x01;
659        assert!(enc.decrypt_aes_gcm(&ciphertext, &nonce, &key).is_err());
660    }
661
662    #[test]
663    fn test_chacha20_tamper_is_rejected() {
664        let enc = PackageEncryptor::new(EncryptionAlgorithm::ChaCha20Poly1305);
665        let key = [0x44u8; 32];
666        let plaintext = b"authenticate me too";
667
668        let (mut ciphertext, nonce) = enc.encrypt_chacha20(plaintext, &key).unwrap();
669        // Flip a single bit in the tag region: authentication MUST fail.
670        let last = ciphertext.len() - 1;
671        ciphertext[last] ^= 0x80;
672        assert!(enc.decrypt_chacha20(&ciphertext, &nonce, &key).is_err());
673    }
674
675    #[test]
676    fn test_pbkdf2_known_derivation_is_stable() {
677        let enc = PackageEncryptor::new(EncryptionAlgorithm::Aes256Gcm);
678
679        // PBKDF2-HMAC-SHA256 known-answer vectors (P="password", S="salt"),
680        // truncated to the 32-byte key length torsh derives. These pin the
681        // algorithm, PRF, and output length; the 100_000-iteration production
682        // path is exercised by the encrypt/decrypt round-trip tests.
683        let derive = |salt: &[u8]| enc.derive_key_from_password("password", salt).unwrap();
684
685        // c is fixed at PBKDF2_ITERATIONS internally, so we validate stability
686        // (same inputs -> same output) plus the documented length + a vector at
687        // the production iteration count computed once and pinned here.
688        let k1 = derive(b"salt");
689        let k2 = derive(b"salt");
690        assert_eq!(k1, k2, "PBKDF2 must be deterministic for identical inputs");
691        assert_eq!(k1.len(), 32, "derived key must be 256-bit");
692        assert_ne!(derive(b"other-salt"), k1, "salt must affect derivation");
693
694        // Pinned known-answer for PBKDF2-HMAC-SHA256(password, salt, 100000, 32),
695        // cross-verified against ring's PBKDF2_HMAC_SHA256.
696        let expected = "0394a2ede332c9a13eb82e9b24631604c31df978b4e2f0fbd2c549944f9d79a5";
697        assert_eq!(
698            hex::encode(&k1),
699            expected,
700            "PBKDF2 production-iteration KAT"
701        );
702    }
703}