Rust · 8424 bytes Raw Blame History
1 use clap::Args;
2 use anyhow::{Context, Result};
3 use dialoguer::{theme::ColorfulTheme, Password};
4 use indicatif::{ProgressBar, ProgressStyle};
5 use std::path::PathBuf;
6 use tracing::info;
7 use humansize::{format_size, BINARY};
8
9 use crate::config::Config;
10 use super::Command;
11
12 #[derive(Debug, Args)]
13 pub struct DecryptCommand {
14 /// Encrypted file to decrypt
15 input: PathBuf,
16
17 /// Output file path (defaults to original filename)
18 #[arg(short, long)]
19 output: Option<PathBuf>,
20
21 /// Password for decryption (will prompt if not provided)
22 #[arg(short, long)]
23 password: Option<String>,
24
25 /// Show progress bar
26 #[arg(long, default_value = "true")]
27 progress: bool,
28
29 /// Skip content verification
30 #[arg(long)]
31 skip_verify: bool,
32
33 /// Force overwrite of existing output file
34 #[arg(long)]
35 force: bool,
36 }
37
38 #[async_trait::async_trait]
39 impl Command for DecryptCommand {
40 async fn execute(&self, _config: &Config) -> Result<()> {
41 info!("Decrypting file: {:?}", self.input);
42
43 // Validate input file
44 if !self.input.exists() {
45 anyhow::bail!("File does not exist: {:?}", self.input);
46 }
47
48 if !self.input.is_file() {
49 anyhow::bail!("Path is not a file: {:?}", self.input);
50 }
51
52 // Read and parse encrypted file
53 let encrypted_data = tokio::fs::read(&self.input).await
54 .with_context(|| format!("Failed to read encrypted file: {:?}", self.input))?;
55
56 let encrypted_file: EncryptedFileFormat = serde_json::from_slice(&encrypted_data)
57 .context("Failed to parse encrypted file format. This may not be a valid ZephyrFS encrypted file.")?;
58
59 // Validate file format version
60 if encrypted_file.version != 1 {
61 anyhow::bail!("Unsupported encrypted file version: {}. This CLI supports version 1.", encrypted_file.version);
62 }
63
64 // Determine output path
65 let output_path = self.output.clone().unwrap_or_else(|| {
66 // Try to restore original filename
67 if encrypted_file.filename != "unknown" {
68 PathBuf::from(&encrypted_file.filename)
69 } else {
70 // Strip .zfs extension if present
71 let mut output = self.input.clone();
72 if let Some(stem) = output.file_stem().map(|s| s.to_os_string()) {
73 output.set_file_name(stem);
74 } else {
75 output.set_extension("decrypted");
76 }
77 output
78 }
79 });
80
81 // Check if output exists
82 if output_path.exists() && !self.force {
83 anyhow::bail!("Output file already exists: {:?}. Use --force to overwrite.", output_path);
84 }
85
86 println!("Decrypting: {}", encrypted_file.filename);
87 println!(" Original size: {}", format_size(encrypted_file.original_size, BINARY));
88 println!(" Segments: {}", encrypted_file.encrypted_segments.len());
89 println!(" Created: {}", encrypted_file.created_at.format("%Y-%m-%d %H:%M:%S UTC"));
90 println!(" Output: {:?}", output_path);
91
92 // Get password
93 let password = if let Some(ref pw) = self.password {
94 pw.clone()
95 } else {
96 let theme = ColorfulTheme::default();
97 Password::with_theme(&theme)
98 .with_prompt("Enter password for decryption")
99 .interact()
100 .context("Failed to read password")?
101 };
102
103 // Create progress bar
104 let progress = if self.progress {
105 let pb = ProgressBar::new(encrypted_file.original_size);
106 pb.set_style(
107 ProgressStyle::default_bar()
108 .template("[{elapsed_precise}] {bar:40.cyan/blue} {percent}% {bytes}/{total_bytes} ETA: {eta}")
109 .unwrap()
110 .progress_chars("##-")
111 );
112 Some(pb)
113 } else {
114 None
115 };
116
117 // Initialize crypto system
118 let crypto = create_crypto_system(&password, encrypted_file.chunk_size_mb)?;
119
120 // Convert segments back to internal format
121 let encrypted_segments: Vec<zephyrfs_node::crypto::EncryptedData> = encrypted_file.encrypted_segments
122 .into_iter()
123 .map(|s| s.into())
124 .collect();
125
126 // Decrypt file
127 let decrypted_data = crypto.decrypt_file(&encrypted_segments)
128 .context("Failed to decrypt file. Check your password and try again.")?;
129
130 // Verify content integrity if requested
131 if !self.skip_verify {
132 println!("Verifying content integrity...");
133 let computed_content_id = crypto.content_id(&decrypted_data);
134 let expected_content_id = zephyrfs_node::crypto::ContentId::from_hex(
135 zephyrfs_node::crypto::HashAlgorithm::Blake3,
136 &encrypted_file.content_id
137 ).context("Invalid content ID in encrypted file")?;
138
139 if !crypto.verify_content(&decrypted_data, &expected_content_id) {
140 anyhow::bail!("Content verification failed! The decrypted file may be corrupted.");
141 }
142 println!("✅ Content integrity verified");
143 }
144
145 // Verify size matches
146 if decrypted_data.len() as u64 != encrypted_file.original_size {
147 anyhow::bail!("Size mismatch: expected {} bytes, got {} bytes",
148 encrypted_file.original_size,
149 decrypted_data.len());
150 }
151
152 // Write decrypted file
153 tokio::fs::write(&output_path, &decrypted_data).await
154 .with_context(|| format!("Failed to write decrypted file: {:?}", output_path))?;
155
156 if let Some(pb) = progress {
157 pb.finish_with_message("Decryption complete");
158 }
159
160 println!("✅ File decrypted successfully!");
161 println!(" Restored file: {:?}", output_path);
162 println!(" Size: {}", format_size(decrypted_data.len() as u64, BINARY));
163
164 // Security cleanup note
165 println!();
166 println!("🔒 Security Notes:");
167 println!(" • The original encrypted file is unchanged");
168 println!(" • Consider securely deleting the encrypted file if no longer needed");
169
170 Ok(())
171 }
172 }
173
174 // Encrypted file format for storage (must match encrypt.rs)
175 #[derive(Debug, serde::Serialize, serde::Deserialize)]
176 struct EncryptedFileFormat {
177 version: u32,
178 filename: String,
179 original_size: u64,
180 chunk_size_mb: u32,
181 content_id: String,
182 hash_algorithm: String,
183 encrypted_segments: Vec<EncryptedSegment>,
184 created_at: chrono::DateTime<chrono::Utc>,
185 }
186
187 #[derive(Debug, serde::Serialize, serde::Deserialize)]
188 struct EncryptedSegment {
189 segment_index: u64,
190 ciphertext: Vec<u8>,
191 nonce: [u8; 12],
192 aad: Vec<u8>,
193 key_path: Vec<u32>,
194 }
195
196 // Helper function to create crypto system (must match encrypt.rs)
197 fn create_crypto_system(password: &str, chunk_size_mb: u32) -> Result<ZephyrCrypto> {
198 use zephyrfs_node::crypto::{ZephyrCrypto, CryptoParams, ScryptParams, AesParams, HashParams, ContentHasher, VerificationHasher};
199
200 // Create crypto parameters (must match encryption parameters)
201 let params = CryptoParams {
202 scrypt_params: ScryptParams {
203 log_n: 17, // Strong security
204 r: 8,
205 p: 1,
206 output_len: 64,
207 },
208 aes_params: AesParams {
209 key_len: 32,
210 nonce_len: 12,
211 tag_len: 16,
212 },
213 hash_params: HashParams {
214 content_hasher: ContentHasher::Blake3,
215 verification_hasher: VerificationHasher::Blake3,
216 },
217 };
218
219 let mut crypto = ZephyrCrypto::with_params(params);
220 crypto.init_from_password(password)
221 .context("Failed to initialize crypto system with password")?;
222
223 Ok(crypto)
224 }
225
226 // Convert between CLI types and zephyrfs_node types
227 impl From<EncryptedSegment> for zephyrfs_node::crypto::EncryptedData {
228 fn from(segment: EncryptedSegment) -> Self {
229 Self {
230 segment_index: segment.segment_index,
231 ciphertext: segment.ciphertext,
232 nonce: segment.nonce,
233 aad: segment.aad,
234 key_path: segment.key_path,
235 }
236 }
237 }
238
239 // Import necessary items from zephyrfs_node
240 use zephyrfs_node::crypto::ZephyrCrypto;