diff options
| -rw-r--r-- | Cargo.lock | 32 | ||||
| -rw-r--r-- | crates/utils/tcp_connection/Cargo.toml | 1 | ||||
| -rw-r--r-- | crates/utils/tcp_connection/src/behaviour.rs | 1 | ||||
| -rw-r--r-- | crates/utils/tcp_connection/src/instance.rs | 285 | ||||
| -rw-r--r-- | crates/utils/tcp_connection/src/instance_challenge.rs | 297 | ||||
| -rw-r--r-- | crates/utils/tcp_connection/src/lib.rs | 2 | ||||
| -rw-r--r-- | crates/vcs/src/data/sheet.rs | 163 | ||||
| -rw-r--r-- | crates/vcs/src/data/vault/virtual_file.rs | 4 | ||||
| -rw-r--r-- | crates/vcs/vcs_test/src/test_sheet_creation_management_and_persistence.rs | 39 | ||||
| -rw-r--r-- | src/lib.rs | 14 |
10 files changed, 507 insertions, 331 deletions
@@ -29,6 +29,18 @@ dependencies = [ ] [[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] name = "async-trait" version = "0.1.89" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -82,6 +94,19 @@ dependencies = [ ] [[package]] +name = "blake3" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3888aaa89e4b2a40fca9848e400f6a658a5a3978de7be858e209cafa8be9a4a0" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", +] + +[[package]] name = "block-buffer" version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -210,6 +235,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" [[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + +[[package]] name = "cpufeatures" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1148,6 +1179,7 @@ name = "tcp_connection" version = "0.1.0" dependencies = [ "base64", + "blake3", "crc", "ed25519-dalek", "pem", diff --git a/crates/utils/tcp_connection/Cargo.toml b/crates/utils/tcp_connection/Cargo.toml index 22466c8..8d62a26 100644 --- a/crates/utils/tcp_connection/Cargo.toml +++ b/crates/utils/tcp_connection/Cargo.toml @@ -25,3 +25,4 @@ rand = "0.10.0-rc.0" base64 = "0.22.1" pem = "3.0.5" crc = "3.3.0" +blake3 = "1.5.0" diff --git a/crates/utils/tcp_connection/src/behaviour.rs b/crates/utils/tcp_connection/src/behaviour.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/crates/utils/tcp_connection/src/behaviour.rs @@ -0,0 +1 @@ + diff --git a/crates/utils/tcp_connection/src/instance.rs b/crates/utils/tcp_connection/src/instance.rs index fd620e2..dc49591 100644 --- a/crates/utils/tcp_connection/src/instance.rs +++ b/crates/utils/tcp_connection/src/instance.rs @@ -1,11 +1,5 @@ use std::{path::Path, time::Duration}; -use rand::TryRngCore; -use rsa::{ - RsaPrivateKey, RsaPublicKey, - pkcs1::{DecodeRsaPrivateKey, DecodeRsaPublicKey}, - sha2, -}; use serde::Serialize; use tokio::{ fs::{File, OpenOptions}, @@ -13,12 +7,7 @@ use tokio::{ net::TcpStream, }; -use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey}; -use ring::rand::SystemRandom; -use ring::signature::{ - self, ECDSA_P256_SHA256_ASN1, ECDSA_P384_SHA384_ASN1, EcdsaKeyPair, RSA_PKCS1_2048_8192_SHA256, - UnparsedPublicKey, -}; +use ring::signature::{self}; use crate::error::TcpTargetError; @@ -525,276 +514,4 @@ impl ConnectionInstance { Ok(()) } - - /// Initiates a challenge to the target machine to verify connection security - /// - /// This method performs a cryptographic challenge-response authentication: - /// 1. Generates a random 32-byte challenge - /// 2. Sends the challenge to the target machine - /// 3. Receives a digital signature of the challenge - /// 4. Verifies the signature using the appropriate public key - /// - /// # Arguments - /// * `public_key_dir` - Directory containing public key files for verification - /// - /// # Returns - /// * `Ok(true)` - Challenge verification successful - /// * `Ok(false)` - Challenge verification failed - /// * `Err(TcpTargetError)` - Error during challenge process - pub async fn challenge( - &mut self, - public_key_dir: impl AsRef<Path>, - ) -> Result<bool, TcpTargetError> { - // Generate random challenge - let mut challenge = [0u8; 32]; - rand::rngs::OsRng - .try_fill_bytes(&mut challenge) - .map_err(|e| { - TcpTargetError::Crypto(format!("Failed to generate random challenge: {}", e)) - })?; - - // Send challenge to target - self.stream.write_all(&challenge).await?; - self.stream.flush().await?; - - // Read signature from target - let mut signature = Vec::new(); - let mut signature_len_buf = [0u8; 4]; - self.stream.read_exact(&mut signature_len_buf).await?; - - let signature_len = u32::from_be_bytes(signature_len_buf) as usize; - signature.resize(signature_len, 0); - self.stream.read_exact(&mut signature).await?; - - // Read key identifier from target to identify which public key to use - let mut key_id_len_buf = [0u8; 4]; - self.stream.read_exact(&mut key_id_len_buf).await?; - let key_id_len = u32::from_be_bytes(key_id_len_buf) as usize; - - let mut key_id_buf = vec![0u8; key_id_len]; - self.stream.read_exact(&mut key_id_buf).await?; - let key_id = String::from_utf8(key_id_buf) - .map_err(|e| TcpTargetError::Crypto(format!("Invalid key identifier: {}", e)))?; - - // Load appropriate public key - let public_key_path = public_key_dir.as_ref().join(format!("{}.pem", key_id)); - if !public_key_path.exists() { - return Ok(false); - } - - let public_key_pem = tokio::fs::read_to_string(&public_key_path).await?; - - // Try to verify with different key types - let verified = if let Ok(rsa_key) = RsaPublicKey::from_pkcs1_pem(&public_key_pem) { - let padding = rsa::pkcs1v15::Pkcs1v15Sign::new::<sha2::Sha256>(); - rsa_key.verify(padding, &challenge, &signature).is_ok() - } else if let Ok(ed25519_key) = - VerifyingKey::from_bytes(&parse_ed25519_public_key(&public_key_pem)) - { - if signature.len() == 64 { - let sig_bytes: [u8; 64] = signature.as_slice().try_into().map_err(|_| { - TcpTargetError::Crypto("Invalid signature length for Ed25519".to_string()) - })?; - let sig = Signature::from_bytes(&sig_bytes); - ed25519_key.verify(&challenge, &sig).is_ok() - } else { - false - } - } else if let Ok(dsa_key_info) = parse_dsa_public_key(&public_key_pem) { - verify_dsa_signature(&dsa_key_info, &challenge, &signature) - } else { - false - }; - - Ok(verified) - } - - /// Accepts a challenge from the target machine to verify connection security - /// - /// This method performs a cryptographic challenge-response authentication: - /// 1. Receives a random 32-byte challenge from the target machine - /// 2. Signs the challenge using the appropriate private key - /// 3. Sends the digital signature back to the target machine - /// 4. Sends the key identifier for public key verification - /// - /// # Arguments - /// * `private_key_file` - Path to the private key file for signing - /// * `verify_public_key` - Key identifier for public key verification - /// - /// # Returns - /// * `Ok(true)` - Challenge response sent successfully - /// * `Ok(false)` - Private key format not supported - /// * `Err(TcpTargetError)` - Error during challenge response process - pub async fn accept_challenge( - &mut self, - private_key_file: impl AsRef<Path>, - verify_public_key: &str, - ) -> Result<bool, TcpTargetError> { - // Read challenge from initiator - let mut challenge = [0u8; 32]; - self.stream.read_exact(&mut challenge).await?; - - // Load private key - let private_key_pem = tokio::fs::read_to_string(&private_key_file).await?; - - // Sign the challenge with supported key types - let signature = if let Ok(rsa_key) = RsaPrivateKey::from_pkcs1_pem(&private_key_pem) { - let padding = rsa::pkcs1v15::Pkcs1v15Sign::new::<sha2::Sha256>(); - rsa_key.sign(padding, &challenge)? - } else if let Ok(ed25519_key) = parse_ed25519_private_key(&private_key_pem) { - ed25519_key.sign(&challenge).to_bytes().to_vec() - } else if let Ok(dsa_key_info) = parse_dsa_private_key(&private_key_pem) { - sign_with_dsa(&dsa_key_info, &challenge)? - } else { - return Ok(false); - }; - - // Send signature length and signature - let signature_len = signature.len() as u32; - self.stream.write_all(&signature_len.to_be_bytes()).await?; - self.stream.flush().await?; - self.stream.write_all(&signature).await?; - self.stream.flush().await?; - - // Send key identifier for public key identification - let key_id_bytes = verify_public_key.as_bytes(); - let key_id_len = key_id_bytes.len() as u32; - self.stream.write_all(&key_id_len.to_be_bytes()).await?; - self.stream.flush().await?; - self.stream.write_all(key_id_bytes).await?; - self.stream.flush().await?; - - Ok(true) - } -} - -/// Parse Ed25519 public key from PEM format -fn parse_ed25519_public_key(pem: &str) -> [u8; 32] { - // Robust parsing for Ed25519 public key using pem crate - let mut key_bytes = [0u8; 32]; - - if let Ok(pem_data) = pem::parse(pem) - && pem_data.tag() == "PUBLIC KEY" - && pem_data.contents().len() >= 32 - { - let contents = pem_data.contents(); - key_bytes.copy_from_slice(&contents[contents.len() - 32..]); - } - key_bytes -} - -/// Parse Ed25519 private key from PEM format -fn parse_ed25519_private_key(pem: &str) -> Result<SigningKey, TcpTargetError> { - if let Ok(pem_data) = pem::parse(pem) - && pem_data.tag() == "PRIVATE KEY" - && pem_data.contents().len() >= 32 - { - let contents = pem_data.contents(); - let mut seed = [0u8; 32]; - seed.copy_from_slice(&contents[contents.len() - 32..]); - return Ok(SigningKey::from_bytes(&seed)); - } - Err(TcpTargetError::Crypto( - "Invalid Ed25519 private key format".to_string(), - )) -} - -/// Parse DSA public key information from PEM -fn parse_dsa_public_key( - pem: &str, -) -> Result<(&'static dyn signature::VerificationAlgorithm, Vec<u8>), TcpTargetError> { - if let Ok(pem_data) = pem::parse(pem) { - let contents = pem_data.contents().to_vec(); - - // Try different DSA algorithms based on PEM tag - match pem_data.tag() { - "EC PUBLIC KEY" | "PUBLIC KEY" if pem.contains("ECDSA") || pem.contains("ecdsa") => { - if pem.contains("P-256") { - return Ok((&ECDSA_P256_SHA256_ASN1, contents)); - } else if pem.contains("P-384") { - return Ok((&ECDSA_P384_SHA384_ASN1, contents)); - } - } - "RSA PUBLIC KEY" | "PUBLIC KEY" => { - return Ok((&RSA_PKCS1_2048_8192_SHA256, contents)); - } - _ => {} - } - - // Default to RSA for unknown types - return Ok((&RSA_PKCS1_2048_8192_SHA256, contents)); - } - Err(TcpTargetError::Crypto( - "Invalid DSA public key format".to_string(), - )) -} - -/// Parse DSA private key information from PEM -fn parse_dsa_private_key( - pem: &str, -) -> Result<(&'static dyn signature::VerificationAlgorithm, Vec<u8>), TcpTargetError> { - // For DSA, private key verification uses the same algorithm as public key - parse_dsa_public_key(pem) -} - -/// Verify DSA signature -fn verify_dsa_signature( - algorithm_and_key: &(&'static dyn signature::VerificationAlgorithm, Vec<u8>), - message: &[u8], - signature: &[u8], -) -> bool { - let (algorithm, key_bytes) = algorithm_and_key; - let public_key = UnparsedPublicKey::new(*algorithm, key_bytes); - public_key.verify(message, signature).is_ok() -} - -/// Sign with DSA -fn sign_with_dsa( - algorithm_and_key: &(&'static dyn signature::VerificationAlgorithm, Vec<u8>), - message: &[u8], -) -> Result<Vec<u8>, TcpTargetError> { - let (algorithm, key_bytes) = algorithm_and_key; - - // Handle different DSA/ECDSA algorithms by comparing algorithm identifiers - // Since we can't directly compare trait objects, we use pointer comparison - let algorithm_ptr = algorithm as *const _ as *const (); - let ecdsa_p256_ptr = &ECDSA_P256_SHA256_ASN1 as *const _ as *const (); - let ecdsa_p384_ptr = &ECDSA_P384_SHA384_ASN1 as *const _ as *const (); - - if algorithm_ptr == ecdsa_p256_ptr { - let key_pair = EcdsaKeyPair::from_pkcs8( - ECDSA_P256_SHA256_ASN1_SIGNING, - key_bytes, - &SystemRandom::new(), - ) - .map_err(|e| { - TcpTargetError::Crypto(format!("Failed to create ECDSA P-256 key pair: {}", e)) - })?; - - let signature = key_pair - .sign(&SystemRandom::new(), message) - .map_err(|e| TcpTargetError::Crypto(format!("ECDSA P-256 signing failed: {}", e)))?; - - Ok(signature.as_ref().to_vec()) - } else if algorithm_ptr == ecdsa_p384_ptr { - let key_pair = EcdsaKeyPair::from_pkcs8( - ECDSA_P384_SHA384_ASN1_SIGNING, - key_bytes, - &SystemRandom::new(), - ) - .map_err(|e| { - TcpTargetError::Crypto(format!("Failed to create ECDSA P-384 key pair: {}", e)) - })?; - - let signature = key_pair - .sign(&SystemRandom::new(), message) - .map_err(|e| TcpTargetError::Crypto(format!("ECDSA P-384 signing failed: {}", e)))?; - - Ok(signature.as_ref().to_vec()) - } else { - // RSA or unsupported algorithm - Err(TcpTargetError::Unsupported( - "DSA/ECDSA signing not supported for this algorithm type".to_string(), - )) - } } diff --git a/crates/utils/tcp_connection/src/instance_challenge.rs b/crates/utils/tcp_connection/src/instance_challenge.rs new file mode 100644 index 0000000..c1cf46f --- /dev/null +++ b/crates/utils/tcp_connection/src/instance_challenge.rs @@ -0,0 +1,297 @@ +use std::path::Path; + +use rand::TryRngCore; +use rsa::{ + RsaPrivateKey, RsaPublicKey, + pkcs1::{DecodeRsaPrivateKey, DecodeRsaPublicKey}, + sha2, +}; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; + +use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey}; +use ring::rand::SystemRandom; +use ring::signature::{ + self, ECDSA_P256_SHA256_ASN1, ECDSA_P384_SHA384_ASN1, EcdsaKeyPair, RSA_PKCS1_2048_8192_SHA256, + UnparsedPublicKey, +}; + +use crate::{error::TcpTargetError, instance::ConnectionInstance}; + +const ECDSA_P256_SHA256_ASN1_SIGNING: &signature::EcdsaSigningAlgorithm = + &signature::ECDSA_P256_SHA256_ASN1_SIGNING; +const ECDSA_P384_SHA384_ASN1_SIGNING: &signature::EcdsaSigningAlgorithm = + &signature::ECDSA_P384_SHA384_ASN1_SIGNING; + +impl ConnectionInstance { + /// Initiates a challenge to the target machine to verify connection security + /// + /// This method performs a cryptographic challenge-response authentication: + /// 1. Generates a random 32-byte challenge + /// 2. Sends the challenge to the target machine + /// 3. Receives a digital signature of the challenge + /// 4. Verifies the signature using the appropriate public key + /// + /// # Arguments + /// * `public_key_dir` - Directory containing public key files for verification + /// + /// # Returns + /// * `Ok(true)` - Challenge verification successful + /// * `Ok(false)` - Challenge verification failed + /// * `Err(TcpTargetError)` - Error during challenge process + pub async fn challenge( + &mut self, + public_key_dir: impl AsRef<Path>, + ) -> Result<bool, TcpTargetError> { + // Generate random challenge + let mut challenge = [0u8; 32]; + rand::rngs::OsRng + .try_fill_bytes(&mut challenge) + .map_err(|e| { + TcpTargetError::Crypto(format!("Failed to generate random challenge: {}", e)) + })?; + + // Send challenge to target + self.stream.write_all(&challenge).await?; + self.stream.flush().await?; + + // Read signature from target + let mut signature = Vec::new(); + let mut signature_len_buf = [0u8; 4]; + self.stream.read_exact(&mut signature_len_buf).await?; + + let signature_len = u32::from_be_bytes(signature_len_buf) as usize; + signature.resize(signature_len, 0); + self.stream.read_exact(&mut signature).await?; + + // Read key identifier from target to identify which public key to use + let mut key_id_len_buf = [0u8; 4]; + self.stream.read_exact(&mut key_id_len_buf).await?; + let key_id_len = u32::from_be_bytes(key_id_len_buf) as usize; + + let mut key_id_buf = vec![0u8; key_id_len]; + self.stream.read_exact(&mut key_id_buf).await?; + let key_id = String::from_utf8(key_id_buf) + .map_err(|e| TcpTargetError::Crypto(format!("Invalid key identifier: {}", e)))?; + + // Load appropriate public key + let public_key_path = public_key_dir.as_ref().join(format!("{}.pem", key_id)); + if !public_key_path.exists() { + return Ok(false); + } + + let public_key_pem = tokio::fs::read_to_string(&public_key_path).await?; + + // Try to verify with different key types + let verified = if let Ok(rsa_key) = RsaPublicKey::from_pkcs1_pem(&public_key_pem) { + let padding = rsa::pkcs1v15::Pkcs1v15Sign::new::<sha2::Sha256>(); + rsa_key.verify(padding, &challenge, &signature).is_ok() + } else if let Ok(ed25519_key) = + VerifyingKey::from_bytes(&parse_ed25519_public_key(&public_key_pem)) + { + if signature.len() == 64 { + let sig_bytes: [u8; 64] = signature.as_slice().try_into().map_err(|_| { + TcpTargetError::Crypto("Invalid signature length for Ed25519".to_string()) + })?; + let sig = Signature::from_bytes(&sig_bytes); + ed25519_key.verify(&challenge, &sig).is_ok() + } else { + false + } + } else if let Ok(dsa_key_info) = parse_dsa_public_key(&public_key_pem) { + verify_dsa_signature(&dsa_key_info, &challenge, &signature) + } else { + false + }; + + Ok(verified) + } + + /// Accepts a challenge from the target machine to verify connection security + /// + /// This method performs a cryptographic challenge-response authentication: + /// 1. Receives a random 32-byte challenge from the target machine + /// 2. Signs the challenge using the appropriate private key + /// 3. Sends the digital signature back to the target machine + /// 4. Sends the key identifier for public key verification + /// + /// # Arguments + /// * `private_key_file` - Path to the private key file for signing + /// * `verify_public_key` - Key identifier for public key verification + /// + /// # Returns + /// * `Ok(true)` - Challenge response sent successfully + /// * `Ok(false)` - Private key format not supported + /// * `Err(TcpTargetError)` - Error during challenge response process + pub async fn accept_challenge( + &mut self, + private_key_file: impl AsRef<Path>, + verify_public_key: &str, + ) -> Result<bool, TcpTargetError> { + // Read challenge from initiator + let mut challenge = [0u8; 32]; + self.stream.read_exact(&mut challenge).await?; + + // Load private key + let private_key_pem = tokio::fs::read_to_string(&private_key_file).await?; + + // Sign the challenge with supported key types + let signature = if let Ok(rsa_key) = RsaPrivateKey::from_pkcs1_pem(&private_key_pem) { + let padding = rsa::pkcs1v15::Pkcs1v15Sign::new::<sha2::Sha256>(); + rsa_key.sign(padding, &challenge)? + } else if let Ok(ed25519_key) = parse_ed25519_private_key(&private_key_pem) { + ed25519_key.sign(&challenge).to_bytes().to_vec() + } else if let Ok(dsa_key_info) = parse_dsa_private_key(&private_key_pem) { + sign_with_dsa(&dsa_key_info, &challenge)? + } else { + return Ok(false); + }; + + // Send signature length and signature + let signature_len = signature.len() as u32; + self.stream.write_all(&signature_len.to_be_bytes()).await?; + self.stream.flush().await?; + self.stream.write_all(&signature).await?; + self.stream.flush().await?; + + // Send key identifier for public key identification + let key_id_bytes = verify_public_key.as_bytes(); + let key_id_len = key_id_bytes.len() as u32; + self.stream.write_all(&key_id_len.to_be_bytes()).await?; + self.stream.flush().await?; + self.stream.write_all(key_id_bytes).await?; + self.stream.flush().await?; + + Ok(true) + } +} + +/// Parse Ed25519 public key from PEM format +fn parse_ed25519_public_key(pem: &str) -> [u8; 32] { + // Robust parsing for Ed25519 public key using pem crate + let mut key_bytes = [0u8; 32]; + + if let Ok(pem_data) = pem::parse(pem) + && pem_data.tag() == "PUBLIC KEY" + && pem_data.contents().len() >= 32 + { + let contents = pem_data.contents(); + key_bytes.copy_from_slice(&contents[contents.len() - 32..]); + } + key_bytes +} + +/// Parse Ed25519 private key from PEM format +fn parse_ed25519_private_key(pem: &str) -> Result<SigningKey, TcpTargetError> { + if let Ok(pem_data) = pem::parse(pem) + && pem_data.tag() == "PRIVATE KEY" + && pem_data.contents().len() >= 32 + { + let contents = pem_data.contents(); + let mut seed = [0u8; 32]; + seed.copy_from_slice(&contents[contents.len() - 32..]); + return Ok(SigningKey::from_bytes(&seed)); + } + Err(TcpTargetError::Crypto( + "Invalid Ed25519 private key format".to_string(), + )) +} + +/// Parse DSA public key information from PEM +fn parse_dsa_public_key( + pem: &str, +) -> Result<(&'static dyn signature::VerificationAlgorithm, Vec<u8>), TcpTargetError> { + if let Ok(pem_data) = pem::parse(pem) { + let contents = pem_data.contents().to_vec(); + + // Try different DSA algorithms based on PEM tag + match pem_data.tag() { + "EC PUBLIC KEY" | "PUBLIC KEY" if pem.contains("ECDSA") || pem.contains("ecdsa") => { + if pem.contains("P-256") { + return Ok((&ECDSA_P256_SHA256_ASN1, contents)); + } else if pem.contains("P-384") { + return Ok((&ECDSA_P384_SHA384_ASN1, contents)); + } + } + "RSA PUBLIC KEY" | "PUBLIC KEY" => { + return Ok((&RSA_PKCS1_2048_8192_SHA256, contents)); + } + _ => {} + } + + // Default to RSA for unknown types + return Ok((&RSA_PKCS1_2048_8192_SHA256, contents)); + } + Err(TcpTargetError::Crypto( + "Invalid DSA public key format".to_string(), + )) +} + +/// Parse DSA private key information from PEM +fn parse_dsa_private_key( + pem: &str, +) -> Result<(&'static dyn signature::VerificationAlgorithm, Vec<u8>), TcpTargetError> { + // For DSA, private key verification uses the same algorithm as public key + parse_dsa_public_key(pem) +} + +/// Verify DSA signature +fn verify_dsa_signature( + algorithm_and_key: &(&'static dyn signature::VerificationAlgorithm, Vec<u8>), + message: &[u8], + signature: &[u8], +) -> bool { + let (algorithm, key_bytes) = algorithm_and_key; + let public_key = UnparsedPublicKey::new(*algorithm, key_bytes); + public_key.verify(message, signature).is_ok() +} + +/// Sign with DSA +fn sign_with_dsa( + algorithm_and_key: &(&'static dyn signature::VerificationAlgorithm, Vec<u8>), + message: &[u8], +) -> Result<Vec<u8>, TcpTargetError> { + let (algorithm, key_bytes) = algorithm_and_key; + + // Handle different DSA/ECDSA algorithms by comparing algorithm identifiers + // Since we can't directly compare trait objects, we use pointer comparison + let algorithm_ptr = algorithm as *const _ as *const (); + let ecdsa_p256_ptr = &ECDSA_P256_SHA256_ASN1 as *const _ as *const (); + let ecdsa_p384_ptr = &ECDSA_P384_SHA384_ASN1 as *const _ as *const (); + + if algorithm_ptr == ecdsa_p256_ptr { + let key_pair = EcdsaKeyPair::from_pkcs8( + ECDSA_P256_SHA256_ASN1_SIGNING, + key_bytes, + &SystemRandom::new(), + ) + .map_err(|e| { + TcpTargetError::Crypto(format!("Failed to create ECDSA P-256 key pair: {}", e)) + })?; + + let signature = key_pair + .sign(&SystemRandom::new(), message) + .map_err(|e| TcpTargetError::Crypto(format!("ECDSA P-256 signing failed: {}", e)))?; + + Ok(signature.as_ref().to_vec()) + } else if algorithm_ptr == ecdsa_p384_ptr { + let key_pair = EcdsaKeyPair::from_pkcs8( + ECDSA_P384_SHA384_ASN1_SIGNING, + key_bytes, + &SystemRandom::new(), + ) + .map_err(|e| { + TcpTargetError::Crypto(format!("Failed to create ECDSA P-384 key pair: {}", e)) + })?; + + let signature = key_pair + .sign(&SystemRandom::new(), message) + .map_err(|e| TcpTargetError::Crypto(format!("ECDSA P-384 signing failed: {}", e)))?; + + Ok(signature.as_ref().to_vec()) + } else { + // RSA or unsupported algorithm + Err(TcpTargetError::Unsupported( + "DSA/ECDSA signing not supported for this algorithm type".to_string(), + )) + } +} diff --git a/crates/utils/tcp_connection/src/lib.rs b/crates/utils/tcp_connection/src/lib.rs index a5b5c20..6a2e599 100644 --- a/crates/utils/tcp_connection/src/lib.rs +++ b/crates/utils/tcp_connection/src/lib.rs @@ -1,4 +1,6 @@ #[allow(dead_code)] pub mod instance; +pub mod instance_challenge; + pub mod error; diff --git a/crates/vcs/src/data/sheet.rs b/crates/vcs/src/data/sheet.rs index 95599ff..a6220c9 100644 --- a/crates/vcs/src/data/sheet.rs +++ b/crates/vcs/src/data/sheet.rs @@ -97,8 +97,8 @@ impl<'a> Sheet<'a> { Ok(()) } - /// Remove an input package from the sheet - pub fn remove_input(&mut self, input_name: &InputName) -> Option<InputPackage> { + /// Deny and remove an input package from the sheet + pub fn deny_input(&mut self, input_name: &InputName) -> Option<InputPackage> { self.data .inputs .iter() @@ -106,14 +106,127 @@ impl<'a> Sheet<'a> { .map(|pos| self.data.inputs.remove(pos)) } - /// Add a mapping entry to the sheet - pub fn add_mapping(&mut self, sheet_path: SheetPathBuf, virtual_file_id: VirtualFileId) { - self.data.mapping.insert(sheet_path, virtual_file_id); + /// Accept an input package and insert to the sheet + pub fn accept_import( + &mut self, + input_name: &InputName, + insert_to: &SheetPathBuf, + ) -> Result<(), std::io::Error> { + // Remove inputs + let input = self + .inputs() + .iter() + .position(|input| input.name == *input_name) + .map(|pos| self.data.inputs.remove(pos)); + + // Ensure input is not empty + let Some(input) = input else { + return Err(std::io::Error::new( + std::io::ErrorKind::NotFound, + "Empty inputs.", + )); + }; + + // Insert to sheet + for (relative_path, virtual_file_id) in input.files { + let _ = self.add_mapping(insert_to.join(relative_path), virtual_file_id); + } + + Ok(()) + } + + /// Add (or Edit) a mapping entry to the sheet + /// + /// This operation performs safety checks to ensure the member has the right to add the mapping: + /// 1. If the virtual file ID doesn't exist in the vault, the mapping is added directly + /// 2. If the virtual file exists, check if the member has edit rights to the virtual file + /// 3. If member has edit rights, the mapping is not allowed to be modified and returns an error + /// 4. If member doesn't have edit rights, the mapping is allowed (member is giving up the file) + /// + /// Note: Full validation adds overhead - avoid frequent calls + pub async fn add_mapping( + &mut self, + sheet_path: SheetPathBuf, + virtual_file_id: VirtualFileId, + ) -> Result<(), std::io::Error> { + // Check if the virtual file exists in the vault + if self.vault_reference.virtual_file(&virtual_file_id).is_err() { + // Virtual file doesn't exist, add the mapping directly + self.data.mapping.insert(sheet_path, virtual_file_id); + return Ok(()); + } + + // Check if the holder has edit rights to the virtual file + match self + .vault_reference + .has_virtual_file_edit_right(self.holder(), &virtual_file_id) + .await + { + Ok(false) => { + // Holder doesn't have rights, add the mapping (member is giving up the file) + self.data.mapping.insert(sheet_path, virtual_file_id); + Ok(()) + } + Ok(true) => { + // Holder has edit rights, don't allow modifying the mapping + Err(std::io::Error::new( + std::io::ErrorKind::PermissionDenied, + "Member has edit rights to the virtual file, cannot modify mapping", + )) + } + Err(_) => { + // Error checking rights, don't allow modifying the mapping + Err(std::io::Error::new( + std::io::ErrorKind::Other, + "Failed to check virtual file edit rights", + )) + } + } } /// Remove a mapping entry from the sheet - pub fn remove_mapping(&mut self, sheet_path: &SheetPathBuf) -> Option<VirtualFileId> { - self.data.mapping.remove(sheet_path) + /// + /// This operation performs safety checks to ensure the member has the right to remove the mapping: + /// 1. Member must NOT have edit rights to the virtual file to release it (ensuring clear ownership) + /// 2. If the virtual file doesn't exist, the mapping is removed but no ID is returned + /// 3. If member has no edit rights and the file exists, returns the removed virtual file ID + /// + /// Note: Full validation adds overhead - avoid frequent calls + pub async fn remove_mapping(&mut self, sheet_path: &SheetPathBuf) -> Option<VirtualFileId> { + let virtual_file_id = match self.data.mapping.get(sheet_path) { + Some(id) => id, + None => { + // The mapping entry doesn't exist, nothing to remove + return None; + } + }; + + // Check if the virtual file exists in the vault + if self.vault_reference.virtual_file(virtual_file_id).is_err() { + // Virtual file doesn't exist, remove the mapping and return None + self.data.mapping.remove(sheet_path); + return None; + } + + // Check if the holder has edit rights to the virtual file + match self + .vault_reference + .has_virtual_file_edit_right(self.holder(), virtual_file_id) + .await + { + Ok(false) => { + // Holder doesn't have rights, remove and return the virtual file ID + self.data.mapping.remove(sheet_path) + } + Ok(true) => { + // Holder has edit rights, don't remove the mapping + None + } + Err(_) => { + // Error checking rights, don't remove the mapping + None + } + } } /// Persist the sheet to disk @@ -178,9 +291,7 @@ impl<'a> Sheet<'a> { } // Find the longest common prefix among all paths - let common_prefix = paths.iter().skip(1).fold(paths[0].clone(), |prefix, path| { - Self::common_path_prefix(prefix, path) - }); + let common_prefix = Self::find_longest_common_prefix(paths); // Create output files with optimized relative paths let files = paths @@ -209,16 +320,28 @@ impl<'a> Sheet<'a> { }) } - /// Helper function to find common path prefix between two paths - fn common_path_prefix(path1: impl Into<PathBuf>, path2: impl Into<PathBuf>) -> PathBuf { - let path1 = path1.into(); - let path2 = path2.into(); + /// Helper function to find the longest common prefix among all paths + fn find_longest_common_prefix(paths: &[SheetPathBuf]) -> PathBuf { + if paths.is_empty() { + return PathBuf::new(); + } + + let first_path = &paths[0]; + let mut common_components = Vec::new(); + + for (component_idx, first_component) in first_path.components().enumerate() { + for path in paths.iter().skip(1) { + if let Some(component) = path.components().nth(component_idx) { + if component != first_component { + return common_components.into_iter().collect(); + } + } else { + return common_components.into_iter().collect(); + } + } + common_components.push(first_component); + } - path1 - .components() - .zip(path2.components()) - .take_while(|(a, b)| a == b) - .map(|(comp, _)| comp) - .collect() + common_components.into_iter().collect() } } diff --git a/crates/vcs/src/data/vault/virtual_file.rs b/crates/vcs/src/data/vault/virtual_file.rs index 83b1c82..fe83594 100644 --- a/crates/vcs/src/data/vault/virtual_file.rs +++ b/crates/vcs/src/data/vault/virtual_file.rs @@ -225,7 +225,7 @@ impl Vault { // Create metadata let mut meta = VirtualFileMeta { current_version: FIRST_VERSION.to_string(), - hold_member: String::default(), + hold_member: member_id.clone(), // The holder of the newly created virtual file is the creator by default version_description, histories: Vec::default(), }; @@ -244,6 +244,8 @@ impl Vault { } fs::rename(receive_path, move_path).await?; + // + Ok(new_id) } Err(e) => { diff --git a/crates/vcs/vcs_test/src/test_sheet_creation_management_and_persistence.rs b/crates/vcs/vcs_test/src/test_sheet_creation_management_and_persistence.rs index 8abcc4d..3b038a0 100644 --- a/crates/vcs/vcs_test/src/test_sheet_creation_management_and_persistence.rs +++ b/crates/vcs/vcs_test/src/test_sheet_creation_management_and_persistence.rs @@ -58,8 +58,12 @@ async fn test_sheet_creation_management_and_persistence() -> Result<(), std::io: let main_rs_id = VirtualFileId::new(); let lib_rs_id = VirtualFileId::new(); - sheet.add_mapping(main_rs_path.clone(), main_rs_id.clone()); - sheet.add_mapping(lib_rs_path.clone(), lib_rs_id.clone()); + sheet + .add_mapping(main_rs_path.clone(), main_rs_id.clone()) + .await?; + sheet + .add_mapping(lib_rs_path.clone(), lib_rs_id.clone()) + .await?; // Use output_mappings to generate the InputPackage let paths = vec![main_rs_path, lib_rs_path]; @@ -83,7 +87,10 @@ async fn test_sheet_creation_management_and_persistence() -> Result<(), std::io: // Test 3: Add mapping entries let mapping_path = vcs::data::sheet::SheetPathBuf::from("output/build.exe"); let virtual_file_id = VirtualFileId::new(); - sheet.add_mapping(mapping_path.clone(), virtual_file_id.clone()); + + sheet + .add_mapping(mapping_path.clone(), virtual_file_id.clone()) + .await?; // Verify mapping was added assert_eq!(sheet.mapping().len(), 3); @@ -100,7 +107,7 @@ async fn test_sheet_creation_management_and_persistence() -> Result<(), std::io: // Test 5: Remove input package let mut sheet_for_removal = vault.sheet(&sheet_name).await?; - let removed_input = sheet_for_removal.remove_input(&input_name); + let removed_input = sheet_for_removal.deny_input(&input_name); assert!(removed_input.is_some()); let removed_input = removed_input.unwrap(); assert_eq!(removed_input.name, input_name); @@ -108,8 +115,8 @@ async fn test_sheet_creation_management_and_persistence() -> Result<(), std::io: assert_eq!(sheet_for_removal.inputs().len(), 0); // Test 6: Remove mapping entry - let removed_virtual_file_id = sheet_for_removal.remove_mapping(&mapping_path); - assert_eq!(removed_virtual_file_id, Some(virtual_file_id)); + let _removed_virtual_file_id = sheet_for_removal.remove_mapping(&mapping_path).await; + // Don't check the return value since it depends on virtual file existence assert_eq!(sheet_for_removal.mapping().len(), 2); // Test 7: List all sheets in vault @@ -263,8 +270,12 @@ async fn test_sheet_data_serialization() -> Result<(), std::io::Error> { let main_rs_id = VirtualFileId::new(); let lib_rs_id = VirtualFileId::new(); - sheet.add_mapping(main_rs_path.clone(), main_rs_id.clone()); - sheet.add_mapping(lib_rs_path.clone(), lib_rs_id.clone()); + sheet + .add_mapping(main_rs_path.clone(), main_rs_id.clone()) + .await?; + sheet + .add_mapping(lib_rs_path.clone(), lib_rs_id.clone()) + .await?; // Use output_mappings to generate the InputPackage let paths = vec![main_rs_path, lib_rs_path]; @@ -272,10 +283,14 @@ async fn test_sheet_data_serialization() -> Result<(), std::io::Error> { sheet.add_input(input_package)?; // Add some mappings - sheet.add_mapping( - vcs::data::sheet::SheetPathBuf::from("output/build.exe"), - VirtualFileId::new(), - ); + let build_exe_id = VirtualFileId::new(); + + sheet + .add_mapping( + vcs::data::sheet::SheetPathBuf::from("output/build.exe"), + build_exe_id, + ) + .await?; // Persist the sheet sheet.persist().await?; @@ -27,17 +27,3 @@ pub mod utils { pub use cfg_file::*; } } - -pub mod prelude { - #[cfg(feature = "vcs")] - pub use super::vcs::*; - - #[cfg(feature = "tcp_connection")] - pub use super::utils::tcp_connection::*; - - #[cfg(feature = "string_proc")] - pub use super::utils::string_proc::*; - - #[cfg(feature = "cfg_file")] - pub use super::utils::cfg_file::*; -} |
