1use 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#[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
43pub const SESSION_KEY_SIZE: usize = 32; pub const NONCE_SIZE: usize = 12; pub const RSA_MIN_KEY_SIZE: usize = 2048; #[derive(Debug)]
50pub struct HybridHeader {
51 pub key_algorithm: KeyAlgorithm,
53 pub encrypted_session_key: Vec<u8>,
55 pub nonce: [u8; NONCE_SIZE],
57 pub metadata: FileMetadata,
59}
60
61impl HybridHeader {
62 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 pub fn to_bytes(&self) -> Vec<u8> {
79 let mut bytes = Vec::new();
80
81 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 let key_len = self.encrypted_session_key.len() as u32;
91 bytes.extend_from_slice(&key_len.to_le_bytes());
92
93 bytes.extend_from_slice(&self.encrypted_session_key);
95
96 bytes.extend_from_slice(&self.nonce);
98
99 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 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 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 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 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 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 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 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
176pub 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 pub fn new() -> Self {
191 Self {
192 crypto: CryptoEngine::new(),
193 ssh_discovery: SshKeyDiscovery::new(),
194 }
195 }
196
197 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 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 fn encrypt_session_key_rsa(
214 &self,
215 session_key: &[u8; SESSION_KEY_SIZE],
216 public_key: &HybridPublicKey,
217 ) -> Result<Vec<u8>, HybridCryptoError> {
218 let openssh_str = public_key.ssh_key.to_openssh()
220 .map_err(|e| HybridCryptoError::SshKeyError(SshKeyError::SshKeyError(e)))?;
221
222 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 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 let mut offset = 0;
250
251 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 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 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 let rsa_key = RsaPublicKey::new(n, e)
285 .map_err(|e| HybridCryptoError::RsaError(e))?;
286
287 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 fn encrypt_session_key_ecdsa(
298 &self,
299 session_key: &[u8; SESSION_KEY_SIZE],
300 public_key: &HybridPublicKey,
301 ) -> Result<Vec<u8>, HybridCryptoError> {
302 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 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 let mut offset = 0;
327
328 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 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 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 let p256_key = P256PublicKey::from_sec1_bytes(point_bytes)
367 .map_err(|e| HybridCryptoError::EcdsaError(format!("Invalid P-256 point: {}", e)))?;
368
369 let ephemeral_secret = EphemeralSecret::random(&mut OsRng);
371 let ephemeral_public = ephemeral_secret.public_key();
372
373 let shared_secret = ephemeral_secret.diffie_hellman(&p256_key);
375
376 let hk = Hkdf::<Sha256>::new(None, shared_secret.raw_secret_bytes());
378 let mut kek = [0u8; 32]; hk.expand(b"sf-cli-hybrid-v1", &mut kek)
380 .map_err(|e| HybridCryptoError::EcdsaError(format!("HKDF expansion failed: {}", e)))?;
381
382 let cipher = aes_gcm::Aes256Gcm::new_from_slice(&kek)
384 .map_err(|e| HybridCryptoError::CryptoError(CryptoError::EncryptionFailed(e.to_string())))?;
385
386 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 let ephemeral_point = ephemeral_public.to_encoded_point(true); 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 pub fn encrypt(
407 &self,
408 data: &[u8],
409 public_key_path: Option<&Path>,
410 metadata: FileMetadata,
411 ) -> Result<Vec<u8>, HybridCryptoError> {
412 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 let session_key = Self::generate_session_key();
426 let mut nonce = [0u8; NONCE_SIZE];
427 OsRng.fill_bytes(&mut nonce);
428
429 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 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 let header = HybridHeader::new(
451 public_key.algorithm,
452 encrypted_session_key,
453 nonce,
454 metadata,
455 );
456
457 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 pub fn decrypt(
468 &self,
469 encrypted_data: &[u8],
470 private_key_path: Option<&Path>,
471 ) -> Result<(Vec<u8>, FileMetadata), HybridCryptoError> {
472 let (header, header_size) = HybridHeader::from_bytes(encrypted_data)?;
474
475 let ciphertext = &encrypted_data[header_size..];
477
478 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 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 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 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 fn decrypt_session_key_rsa(
525 &self,
526 encrypted_session_key: &[u8],
527 private_key: &HybridPrivateKey,
528 ) -> Result<[u8; SESSION_KEY_SIZE], HybridCryptoError> {
529 let _openssh_str = private_key.ssh_key.to_openssh(ssh_key::LineEnding::LF)
531 .map_err(|e| HybridCryptoError::SshKeyError(SshKeyError::SshKeyError(e)))?;
532
533 let ssh_private_key = &private_key.ssh_key;
535
536 match ssh_private_key.key_data() {
539 ssh_key::private::KeypairData::Rsa(rsa_keypair) => {
540 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 let rsa_private_key = RsaPrivateKey::from_components(n, e, d, primes)
551 .map_err(|e| HybridCryptoError::RsaError(e))?;
552
553 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 fn decrypt_session_key_ecdsa(
574 &self,
575 encrypted_data: &[u8],
576 private_key: &HybridPrivateKey,
577 ) -> Result<[u8; SESSION_KEY_SIZE], HybridCryptoError> {
578 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 let _ephemeral_public = P256PublicKey::from_sec1_bytes(ephemeral_public_bytes)
589 .map_err(|e| HybridCryptoError::EcdsaError(format!("Invalid ephemeral public key: {}", e)))?;
590
591 match private_key.ssh_key.key_data() {
593 ssh_key::private::KeypairData::Ecdsa(_ecdsa_keypair) => {
594 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 pub fn extract_public_key_info(&self, encrypted_data: &[u8]) -> Result<(KeyAlgorithm, String), HybridCryptoError> {
607 let (header, _) = HybridHeader::from_bytes(encrypted_data)?;
608
609 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 assert_ne!(key1, key2);
633
634 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]; 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 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}