sf_cli/
hybrid_crypto.rs

1//! Hybrid encryption module combining asymmetric and symmetric cryptography
2
3use crate::{
4    crypto::{CryptoEngine, CryptoError, FileMetadata},
5    ssh_keys::{HybridPublicKey, HybridPrivateKey, KeyAlgorithm, SshKeyDiscovery, SshKeyError},
6};
7use aes_gcm::{
8    aead::{Aead, KeyInit, OsRng},
9    Aes256Gcm, Nonce,
10};
11use base64::{Engine as _, engine::general_purpose};
12use rand::RngCore;
13use rsa::{Pkcs1v15Encrypt, RsaPublicKey, RsaPrivateKey};
14use p256::{
15    ecdh::EphemeralSecret,
16    PublicKey as P256PublicKey,
17    elliptic_curve::sec1::ToEncodedPoint,
18};
19use sha2::Sha256;
20use hkdf::Hkdf;
21use std::path::Path;
22use thiserror::Error;
23
24/// Hybrid encryption errors
25#[derive(Error, Debug)]
26pub enum HybridCryptoError {
27    #[error("SSH key error: {0}")]
28    SshKeyError(#[from] SshKeyError),
29    #[error("Crypto error: {0}")]
30    CryptoError(#[from] CryptoError),
31    #[error("RSA encryption error: {0}")]
32    RsaError(#[from] rsa::Error),
33    #[error("ECDSA error: {0}")]
34    EcdsaError(String),
35    #[error("Invalid hybrid file format")]
36    InvalidFormat,
37    #[error("Unsupported key algorithm: {0}")]
38    UnsupportedAlgorithm(String),
39    #[error("Invalid encrypted session key length")]
40    InvalidSessionKeyLength,
41}
42
43/// Size constants for hybrid encryption
44pub const SESSION_KEY_SIZE: usize = 32; // 256 bits for AES-256
45pub const NONCE_SIZE: usize = 12; // 96 bits for GCM
46pub const RSA_MIN_KEY_SIZE: usize = 2048; // Minimum RSA key size in bits
47
48/// Hybrid encryption header containing encrypted session key and metadata
49#[derive(Debug)]
50pub struct HybridHeader {
51    /// Algorithm used for session key encryption
52    pub key_algorithm: KeyAlgorithm,
53    /// Encrypted session key
54    pub encrypted_session_key: Vec<u8>,
55    /// Nonce for AES-GCM encryption
56    pub nonce: [u8; NONCE_SIZE],
57    /// File metadata
58    pub metadata: FileMetadata,
59}
60
61impl HybridHeader {
62    /// Create a new hybrid header
63    pub fn new(
64        key_algorithm: KeyAlgorithm,
65        encrypted_session_key: Vec<u8>,
66        nonce: [u8; NONCE_SIZE],
67        metadata: FileMetadata,
68    ) -> Self {
69        Self {
70            key_algorithm,
71            encrypted_session_key,
72            nonce,
73            metadata,
74        }
75    }
76
77    /// Serialize header to bytes
78    pub fn to_bytes(&self) -> Vec<u8> {
79        let mut bytes = Vec::new();
80        
81        // Algorithm type (1 byte)
82        let alg_byte = match self.key_algorithm {
83            KeyAlgorithm::Rsa => 0x01,
84            KeyAlgorithm::EcdsaP256 => 0x02,
85            KeyAlgorithm::Ed25519 => 0x03,
86        };
87        bytes.push(alg_byte);
88        
89        // Encrypted session key length (4 bytes)
90        let key_len = self.encrypted_session_key.len() as u32;
91        bytes.extend_from_slice(&key_len.to_le_bytes());
92        
93        // Encrypted session key
94        bytes.extend_from_slice(&self.encrypted_session_key);
95        
96        // Nonce
97        bytes.extend_from_slice(&self.nonce);
98        
99        // Metadata
100        let metadata_bytes = self.metadata.to_bytes();
101        let metadata_len = metadata_bytes.len() as u32;
102        bytes.extend_from_slice(&metadata_len.to_le_bytes());
103        bytes.extend_from_slice(&metadata_bytes);
104        
105        bytes
106    }
107
108    /// Deserialize header from bytes
109    pub fn from_bytes(bytes: &[u8]) -> Result<(Self, usize), HybridCryptoError> {
110        if bytes.len() < 1 + 4 + NONCE_SIZE + 4 {
111            return Err(HybridCryptoError::InvalidFormat);
112        }
113
114        let mut offset = 0;
115        
116        // Algorithm type
117        let alg_byte = bytes[offset];
118        offset += 1;
119        let key_algorithm = match alg_byte {
120            0x01 => KeyAlgorithm::Rsa,
121            0x02 => KeyAlgorithm::EcdsaP256,
122            0x03 => KeyAlgorithm::Ed25519,
123            _ => return Err(HybridCryptoError::UnsupportedAlgorithm(format!("Unknown algorithm byte: {}", alg_byte))),
124        };
125        
126        // Encrypted session key length
127        if bytes.len() < offset + 4 {
128            return Err(HybridCryptoError::InvalidFormat);
129        }
130        let key_len = u32::from_le_bytes([
131            bytes[offset],
132            bytes[offset + 1],
133            bytes[offset + 2],
134            bytes[offset + 3],
135        ]) as usize;
136        offset += 4;
137        
138        // Encrypted session key
139        if bytes.len() < offset + key_len {
140            return Err(HybridCryptoError::InvalidFormat);
141        }
142        let encrypted_session_key = bytes[offset..offset + key_len].to_vec();
143        offset += key_len;
144        
145        // Nonce
146        if bytes.len() < offset + NONCE_SIZE {
147            return Err(HybridCryptoError::InvalidFormat);
148        }
149        let mut nonce = [0u8; NONCE_SIZE];
150        nonce.copy_from_slice(&bytes[offset..offset + NONCE_SIZE]);
151        offset += NONCE_SIZE;
152        
153        // Metadata length
154        if bytes.len() < offset + 4 {
155            return Err(HybridCryptoError::InvalidFormat);
156        }
157        let metadata_len = u32::from_le_bytes([
158            bytes[offset],
159            bytes[offset + 1],
160            bytes[offset + 2],
161            bytes[offset + 3],
162        ]) as usize;
163        offset += 4;
164        
165        // Metadata
166        if bytes.len() < offset + metadata_len {
167            return Err(HybridCryptoError::InvalidFormat);
168        }
169        let (metadata, _) = FileMetadata::from_bytes(&bytes[offset..offset + metadata_len])?;
170        offset += metadata_len;
171        
172        Ok((Self::new(key_algorithm, encrypted_session_key, nonce, metadata), offset))
173    }
174}
175
176/// Hybrid crypto engine for encryption/decryption operations
177pub struct HybridCryptoEngine {
178    crypto: CryptoEngine,
179    ssh_discovery: SshKeyDiscovery,
180}
181
182impl Default for HybridCryptoEngine {
183    fn default() -> Self {
184        Self::new()
185    }
186}
187
188impl HybridCryptoEngine {
189    /// Create a new hybrid crypto engine
190    pub fn new() -> Self {
191        Self {
192            crypto: CryptoEngine::new(),
193            ssh_discovery: SshKeyDiscovery::new(),
194        }
195    }
196
197    /// Create a new hybrid crypto engine with custom SSH directory
198    pub fn with_ssh_dir<P: AsRef<Path>>(ssh_dir: P) -> Self {
199        Self {
200            crypto: CryptoEngine::new(),
201            ssh_discovery: SshKeyDiscovery::with_ssh_dir(ssh_dir),
202        }
203    }
204
205    /// Generate a random session key for AES-256
206    fn generate_session_key() -> [u8; SESSION_KEY_SIZE] {
207        let mut key = [0u8; SESSION_KEY_SIZE];
208        OsRng.fill_bytes(&mut key);
209        key
210    }
211
212    /// Encrypt session key with RSA public key
213    fn encrypt_session_key_rsa(
214        &self,
215        session_key: &[u8; SESSION_KEY_SIZE],
216        public_key: &HybridPublicKey,
217    ) -> Result<Vec<u8>, HybridCryptoError> {
218        // Convert SSH key to OpenSSH format and then parse manually
219        let openssh_str = public_key.ssh_key.to_openssh()
220            .map_err(|e| HybridCryptoError::SshKeyError(SshKeyError::SshKeyError(e)))?;
221        
222        // For SSH RSA keys, we need to extract the RSA public key components
223        // The SSH key format is: algorithm_name public_key_blob comment
224        // The public_key_blob is base64 encoded and contains:
225        // - algorithm name (string)
226        // - public exponent e (mpint)
227        // - modulus n (mpint)
228        
229        let parts: Vec<&str> = openssh_str.trim().split_whitespace().collect();
230        if parts.len() < 2 {
231            return Err(HybridCryptoError::UnsupportedAlgorithm(
232                "Invalid SSH key format".to_string()
233            ));
234        }
235        
236        if parts[0] != "ssh-rsa" {
237            return Err(HybridCryptoError::UnsupportedAlgorithm(
238                format!("Expected ssh-rsa but got {}", parts[0])
239            ));
240        }
241        
242        // Decode the base64 blob
243        let blob = general_purpose::STANDARD.decode(parts[1])
244            .map_err(|e| HybridCryptoError::UnsupportedAlgorithm(
245                format!("Failed to decode SSH key blob: {}", e)
246            ))?;
247        
248        // Parse the SSH wire format
249        let mut offset = 0;
250        
251        // Skip algorithm name (string)
252        if blob.len() < offset + 4 {
253            return Err(HybridCryptoError::UnsupportedAlgorithm("Invalid blob format".to_string()));
254        }
255        let name_len = u32::from_be_bytes([blob[offset], blob[offset+1], blob[offset+2], blob[offset+3]]) as usize;
256        offset += 4 + name_len;
257        
258        // Read public exponent e (mpint)
259        if blob.len() < offset + 4 {
260            return Err(HybridCryptoError::UnsupportedAlgorithm("Invalid blob format".to_string()));
261        }
262        let e_len = u32::from_be_bytes([blob[offset], blob[offset+1], blob[offset+2], blob[offset+3]]) as usize;
263        offset += 4;
264        if blob.len() < offset + e_len {
265            return Err(HybridCryptoError::UnsupportedAlgorithm("Invalid blob format".to_string()));
266        }
267        let e_bytes = &blob[offset..offset + e_len];
268        let e = rsa::BigUint::from_bytes_be(e_bytes);
269        offset += e_len;
270        
271        // Read modulus n (mpint)
272        if blob.len() < offset + 4 {
273            return Err(HybridCryptoError::UnsupportedAlgorithm("Invalid blob format".to_string()));
274        }
275        let n_len = u32::from_be_bytes([blob[offset], blob[offset+1], blob[offset+2], blob[offset+3]]) as usize;
276        offset += 4;
277        if blob.len() < offset + n_len {
278            return Err(HybridCryptoError::UnsupportedAlgorithm("Invalid blob format".to_string()));
279        }
280        let n_bytes = &blob[offset..offset + n_len];
281        let n = rsa::BigUint::from_bytes_be(n_bytes);
282        
283        // Create RSA public key
284        let rsa_key = RsaPublicKey::new(n, e)
285            .map_err(|e| HybridCryptoError::RsaError(e))?;
286        
287        // Encrypt session key using PKCS#1 v1.5 padding
288        let mut rng = OsRng;
289        let encrypted_key = rsa_key
290            .encrypt(&mut rng, Pkcs1v15Encrypt, session_key)
291            .map_err(|e| HybridCryptoError::RsaError(e))?;
292
293        Ok(encrypted_key)
294    }
295
296    /// Encrypt session key with ECDSA P-256 public key using ECDH
297    fn encrypt_session_key_ecdsa(
298        &self,
299        session_key: &[u8; SESSION_KEY_SIZE],
300        public_key: &HybridPublicKey,
301    ) -> Result<Vec<u8>, HybridCryptoError> {
302        // Convert SSH key to OpenSSH format and parse ECDSA key
303        let openssh_str = public_key.ssh_key.to_openssh()
304            .map_err(|e| HybridCryptoError::SshKeyError(SshKeyError::SshKeyError(e)))?;
305        
306        let parts: Vec<&str> = openssh_str.trim().split_whitespace().collect();
307        if parts.len() < 2 {
308            return Err(HybridCryptoError::UnsupportedAlgorithm(
309                "Invalid SSH key format".to_string()
310            ));
311        }
312        
313        if !parts[0].starts_with("ecdsa-sha2-") {
314            return Err(HybridCryptoError::UnsupportedAlgorithm(
315                format!("Expected ecdsa-sha2-* but got {}", parts[0])
316            ));
317        }
318        
319        // Decode the base64 blob
320        let blob = general_purpose::STANDARD.decode(parts[1])
321            .map_err(|e| HybridCryptoError::UnsupportedAlgorithm(
322                format!("Failed to decode SSH key blob: {}", e)
323            ))?;
324        
325        // Parse the SSH wire format for ECDSA
326        let mut offset = 0;
327        
328        // Skip algorithm name (string)
329        if blob.len() < offset + 4 {
330            return Err(HybridCryptoError::UnsupportedAlgorithm("Invalid blob format".to_string()));
331        }
332        let name_len = u32::from_be_bytes([blob[offset], blob[offset+1], blob[offset+2], blob[offset+3]]) as usize;
333        offset += 4 + name_len;
334        
335        // Read curve name (string)
336        if blob.len() < offset + 4 {
337            return Err(HybridCryptoError::UnsupportedAlgorithm("Invalid blob format".to_string()));
338        }
339        let curve_len = u32::from_be_bytes([blob[offset], blob[offset+1], blob[offset+2], blob[offset+3]]) as usize;
340        offset += 4;
341        if blob.len() < offset + curve_len {
342            return Err(HybridCryptoError::UnsupportedAlgorithm("Invalid blob format".to_string()));
343        }
344        let curve_name = std::str::from_utf8(&blob[offset..offset + curve_len])
345            .map_err(|e| HybridCryptoError::UnsupportedAlgorithm(format!("Invalid curve name: {}", e)))?;
346        offset += curve_len;
347        
348        if curve_name != "nistp256" {
349            return Err(HybridCryptoError::UnsupportedAlgorithm(
350                format!("Unsupported ECDSA curve: {}", curve_name)
351            ));
352        }
353        
354        // Read public key point (string)
355        if blob.len() < offset + 4 {
356            return Err(HybridCryptoError::UnsupportedAlgorithm("Invalid blob format".to_string()));
357        }
358        let point_len = u32::from_be_bytes([blob[offset], blob[offset+1], blob[offset+2], blob[offset+3]]) as usize;
359        offset += 4;
360        if blob.len() < offset + point_len {
361            return Err(HybridCryptoError::UnsupportedAlgorithm("Invalid blob format".to_string()));
362        }
363        let point_bytes = &blob[offset..offset + point_len];
364        
365        // Parse the P-256 public key from the uncompressed point format
366        let p256_key = P256PublicKey::from_sec1_bytes(point_bytes)
367            .map_err(|e| HybridCryptoError::EcdsaError(format!("Invalid P-256 point: {}", e)))?;
368
369        // Generate ephemeral key pair for ECDH
370        let ephemeral_secret = EphemeralSecret::random(&mut OsRng);
371        let ephemeral_public = ephemeral_secret.public_key();
372        
373        // Perform ECDH to get shared secret
374        let shared_secret = ephemeral_secret.diffie_hellman(&p256_key);
375        
376        // Use HKDF to derive key encryption key from shared secret
377        let hk = Hkdf::<Sha256>::new(None, shared_secret.raw_secret_bytes());
378        let mut kek = [0u8; 32]; // Key encryption key
379        hk.expand(b"sf-cli-hybrid-v1", &mut kek)
380            .map_err(|e| HybridCryptoError::EcdsaError(format!("HKDF expansion failed: {}", e)))?;
381
382        // Encrypt session key with AES-256
383        let cipher = aes_gcm::Aes256Gcm::new_from_slice(&kek)
384            .map_err(|e| HybridCryptoError::CryptoError(CryptoError::EncryptionFailed(e.to_string())))?;
385        
386        // Generate nonce for session key encryption
387        let mut key_nonce = [0u8; 12];
388        OsRng.fill_bytes(&mut key_nonce);
389        
390        let nonce_obj = aes_gcm::Nonce::from_slice(&key_nonce);
391        let encrypted_session_key = cipher
392            .encrypt(nonce_obj, session_key.as_ref())
393            .map_err(|e| HybridCryptoError::CryptoError(CryptoError::EncryptionFailed(e.to_string())))?;
394
395        // Return: ephemeral_public_key (33 bytes) + nonce (12 bytes) + encrypted_session_key
396        let ephemeral_point = ephemeral_public.to_encoded_point(true); // Compressed format
397        let mut result = Vec::with_capacity(33 + 12 + encrypted_session_key.len());
398        result.extend_from_slice(ephemeral_point.as_bytes());
399        result.extend_from_slice(&key_nonce);
400        result.extend_from_slice(&encrypted_session_key);
401        
402        Ok(result)
403    }
404
405    /// Encrypt data with hybrid encryption
406    pub fn encrypt(
407        &self,
408        data: &[u8],
409        public_key_path: Option<&Path>,
410        metadata: FileMetadata,
411    ) -> Result<Vec<u8>, HybridCryptoError> {
412        // Discover or load public key
413        let public_key = match public_key_path {
414            Some(path) => {
415                println!("🔑 Using public key from: {}", path.display());
416                self.ssh_discovery.load_public_key_from_path(path)?
417            },
418            None => {
419                println!("🔍 Auto-discovering public keys...");
420                self.ssh_discovery.select_public_key_interactive()?
421            },
422        };
423
424        // Generate session key and nonce
425        let session_key = Self::generate_session_key();
426        let mut nonce = [0u8; NONCE_SIZE];
427        OsRng.fill_bytes(&mut nonce);
428
429        // Encrypt session key with public key
430        let encrypted_session_key = match public_key.algorithm {
431            KeyAlgorithm::Rsa => self.encrypt_session_key_rsa(&session_key, &public_key)?,
432            KeyAlgorithm::EcdsaP256 => self.encrypt_session_key_ecdsa(&session_key, &public_key)?,
433            KeyAlgorithm::Ed25519 => {
434                return Err(HybridCryptoError::UnsupportedAlgorithm(
435                    "Ed25519 encryption not yet implemented".to_string()
436                ));
437            }
438        };
439
440        // Encrypt data with AES-256-GCM using session key
441        let cipher = Aes256Gcm::new_from_slice(&session_key)
442            .map_err(|e| HybridCryptoError::CryptoError(CryptoError::EncryptionFailed(e.to_string())))?;
443        
444        let nonce_obj = Nonce::from_slice(&nonce);
445        let ciphertext = cipher
446            .encrypt(nonce_obj, data)
447            .map_err(|e| HybridCryptoError::CryptoError(CryptoError::EncryptionFailed(e.to_string())))?;
448
449        // Create header
450        let header = HybridHeader::new(
451            public_key.algorithm,
452            encrypted_session_key,
453            nonce,
454            metadata,
455        );
456
457        // Combine header and ciphertext
458        let header_bytes = header.to_bytes();
459        let mut result = Vec::with_capacity(header_bytes.len() + ciphertext.len());
460        result.extend_from_slice(&header_bytes);
461        result.extend_from_slice(&ciphertext);
462
463        Ok(result)
464    }
465
466    /// Hybrid decryption functionality
467    pub fn decrypt(
468        &self,
469        encrypted_data: &[u8],
470        private_key_path: Option<&Path>,
471    ) -> Result<(Vec<u8>, FileMetadata), HybridCryptoError> {
472        // Parse the header first
473        let (header, header_size) = HybridHeader::from_bytes(encrypted_data)?;
474        
475        // The remaining data is the encrypted content
476        let ciphertext = &encrypted_data[header_size..];
477        
478        // Discover or load private key
479        let private_key = match private_key_path {
480            Some(path) => {
481                println!("🔑 Using private key from: {}", path.display());
482                self.ssh_discovery.load_private_key_from_path(path)?
483            },
484            None => {
485                println!("🔍 Auto-discovering private keys...");
486                self.ssh_discovery.select_private_key_interactive()?
487            },
488        };
489        
490        // Verify that the private key algorithm matches the header
491        if private_key.algorithm != header.key_algorithm {
492            return Err(HybridCryptoError::UnsupportedAlgorithm(
493                format!("Private key algorithm ({}) does not match encrypted file algorithm ({})",
494                    private_key.algorithm, header.key_algorithm)
495            ));
496        }
497        
498        println!("🔓 Decrypting with key: {}", private_key.display_name());
499        
500        // Decrypt session key using private key
501        let session_key = match header.key_algorithm {
502            KeyAlgorithm::Rsa => self.decrypt_session_key_rsa(&header.encrypted_session_key, &private_key)?,
503            KeyAlgorithm::EcdsaP256 => self.decrypt_session_key_ecdsa(&header.encrypted_session_key, &private_key)?,
504            KeyAlgorithm::Ed25519 => {
505                return Err(HybridCryptoError::UnsupportedAlgorithm(
506                    "Ed25519 decryption not yet implemented".to_string()
507                ));
508            }
509        };
510        
511        // Decrypt data with AES-256-GCM using session key
512        let cipher = Aes256Gcm::new_from_slice(&session_key)
513            .map_err(|e| HybridCryptoError::CryptoError(CryptoError::DecryptionFailed(e.to_string())))?;
514        
515        let nonce_obj = Nonce::from_slice(&header.nonce);
516        let plaintext = cipher
517            .decrypt(nonce_obj, ciphertext)
518            .map_err(|e| HybridCryptoError::CryptoError(CryptoError::DecryptionFailed(e.to_string())))?;
519
520        Ok((plaintext, header.metadata))
521    }
522
523    /// Decrypt session key with RSA private key
524    fn decrypt_session_key_rsa(
525        &self,
526        encrypted_session_key: &[u8],
527        private_key: &HybridPrivateKey,
528    ) -> Result<[u8; SESSION_KEY_SIZE], HybridCryptoError> {
529        // Convert SSH private key to RSA format
530        let _openssh_str = private_key.ssh_key.to_openssh(ssh_key::LineEnding::LF)
531            .map_err(|e| HybridCryptoError::SshKeyError(SshKeyError::SshKeyError(e)))?;
532        
533        // For SSH RSA private keys, we need to extract the components
534        let ssh_private_key = &private_key.ssh_key;
535        
536        // Get the RSA key data from the SSH private key
537        // We'll use the ssh-key crate's built-in conversion capabilities
538        match ssh_private_key.key_data() {
539            ssh_key::private::KeypairData::Rsa(rsa_keypair) => {
540                // Extract RSA components
541                let n = rsa::BigUint::from_bytes_be(rsa_keypair.public.n.as_bytes());
542                let e = rsa::BigUint::from_bytes_be(rsa_keypair.public.e.as_bytes());
543                let d = rsa::BigUint::from_bytes_be(rsa_keypair.private.d.as_bytes());
544                let primes = vec![
545                    rsa::BigUint::from_bytes_be(rsa_keypair.private.p.as_bytes()),
546                    rsa::BigUint::from_bytes_be(rsa_keypair.private.q.as_bytes()),
547                ];
548                
549                // Create RSA private key
550                let rsa_private_key = RsaPrivateKey::from_components(n, e, d, primes)
551                    .map_err(|e| HybridCryptoError::RsaError(e))?;
552                
553                // Decrypt session key using PKCS#1 v1.5 padding
554                let decrypted_key = rsa_private_key
555                    .decrypt(Pkcs1v15Encrypt, encrypted_session_key)
556                    .map_err(|e| HybridCryptoError::RsaError(e))?;
557                
558                if decrypted_key.len() != SESSION_KEY_SIZE {
559                    return Err(HybridCryptoError::InvalidSessionKeyLength);
560                }
561                
562                let mut session_key = [0u8; SESSION_KEY_SIZE];
563                session_key.copy_from_slice(&decrypted_key);
564                Ok(session_key)
565            }
566            _ => Err(HybridCryptoError::UnsupportedAlgorithm(
567                "Expected RSA private key".to_string()
568            ))
569        }
570    }
571
572    /// Decrypt session key with ECDSA P-256 private key using ECDH
573    fn decrypt_session_key_ecdsa(
574        &self,
575        encrypted_data: &[u8],
576        private_key: &HybridPrivateKey,
577    ) -> Result<[u8; SESSION_KEY_SIZE], HybridCryptoError> {
578        // Extract ephemeral public key (33 bytes) + nonce (12 bytes) + encrypted session key
579        if encrypted_data.len() < 33 + 12 {
580            return Err(HybridCryptoError::InvalidFormat);
581        }
582        
583        let ephemeral_public_bytes = &encrypted_data[0..33];
584        let _key_nonce = &encrypted_data[33..45];
585        let _encrypted_session_key = &encrypted_data[45..];
586        
587        // Parse ephemeral public key
588        let _ephemeral_public = P256PublicKey::from_sec1_bytes(ephemeral_public_bytes)
589            .map_err(|e| HybridCryptoError::EcdsaError(format!("Invalid ephemeral public key: {}", e)))?;
590        
591        // Get our private key
592        match private_key.ssh_key.key_data() {
593            ssh_key::private::KeypairData::Ecdsa(_ecdsa_keypair) => {
594                // For now, return an error until we can properly access the private key
595                return Err(HybridCryptoError::EcdsaError(
596                    "ECDSA private key access needs to be implemented with correct field names".to_string()
597                ));
598            }
599            _ => Err(HybridCryptoError::UnsupportedAlgorithm(
600                "Expected ECDSA private key".to_string()
601            ))
602        }
603    }
604
605    /// Extract public key information from an encrypted file
606    pub fn extract_public_key_info(&self, encrypted_data: &[u8]) -> Result<(KeyAlgorithm, String), HybridCryptoError> {
607        let (header, _) = HybridHeader::from_bytes(encrypted_data)?;
608        
609        // Create a description of the key used for encryption
610        let key_description = match header.key_algorithm {
611            KeyAlgorithm::Rsa => "RSA public key".to_string(),
612            KeyAlgorithm::EcdsaP256 => "ECDSA P-256 public key".to_string(),
613            KeyAlgorithm::Ed25519 => "Ed25519 public key".to_string(),
614        };
615        
616        Ok((header.key_algorithm, key_description))
617    }
618
619}
620
621#[cfg(test)]
622mod tests {
623    use super::*;
624    use tempfile::TempDir;
625
626    #[test]
627    fn test_session_key_generation() {
628        let key1 = HybridCryptoEngine::generate_session_key();
629        let key2 = HybridCryptoEngine::generate_session_key();
630        
631        // Keys should be different
632        assert_ne!(key1, key2);
633        
634        // Keys should be the right size
635        assert_eq!(key1.len(), SESSION_KEY_SIZE);
636        assert_eq!(key2.len(), SESSION_KEY_SIZE);
637    }
638
639    #[test]
640    fn test_hybrid_header_serialization() {
641        let metadata = FileMetadata::new("test.txt".to_string(), [42u8; 32], false);
642        let encrypted_key = vec![1, 2, 3, 4]; // Dummy encrypted key
643        let nonce = [5u8; NONCE_SIZE];
644        
645        let header = HybridHeader::new(
646            KeyAlgorithm::Rsa,
647            encrypted_key.clone(),
648            nonce,
649            metadata.clone(),
650        );
651        
652        let bytes = header.to_bytes();
653        let (recovered, size) = HybridHeader::from_bytes(&bytes).unwrap();
654        
655        assert_eq!(size, bytes.len());
656        assert_eq!(recovered.key_algorithm, KeyAlgorithm::Rsa);
657        assert_eq!(recovered.encrypted_session_key, encrypted_key);
658        assert_eq!(recovered.nonce, nonce);
659        assert_eq!(recovered.metadata.filename, metadata.filename);
660    }
661
662    #[test]
663    fn test_hybrid_crypto_engine_creation() {
664        let engine = HybridCryptoEngine::new();
665        
666        // Just test that it was created successfully
667        // We can't test much more without actual SSH keys
668        assert!(true);
669    }
670
671    #[test]
672    fn test_invalid_hybrid_format() {
673        let invalid_data = b"not_hybrid_data";
674        let result = HybridHeader::from_bytes(invalid_data);
675        assert!(matches!(result, Err(HybridCryptoError::InvalidFormat)));
676    }
677}