Rust · 8041 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 EncryptCommand {
14 /// File to encrypt
15 input: PathBuf,
16
17 /// Output file path (defaults to input.zfs)
18 #[arg(short, long)]
19 output: Option<PathBuf>,
20
21 /// Password for encryption (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 /// Verification hash algorithm (blake3, sha256, both)
30 #[arg(long, default_value = "both")]
31 hash: String,
32
33 /// Chunk size in MB for large files
34 #[arg(long, default_value = "1")]
35 chunk_size: u32,
36 }
37
38 #[async_trait::async_trait]
39 impl Command for EncryptCommand {
40 async fn execute(&self, _config: &Config) -> Result<()> {
41 info!("Encrypting 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 let metadata = tokio::fs::metadata(&self.input).await
53 .with_context(|| format!("Failed to read file metadata: {:?}", self.input))?;
54
55 let file_size = metadata.len();
56 let display_name = self.input.file_name()
57 .and_then(|n| n.to_str())
58 .unwrap_or("unknown");
59
60 // Determine output path
61 let output_path = self.output.clone().unwrap_or_else(|| {
62 let mut output = self.input.clone();
63 let current_ext = output.extension()
64 .and_then(|s| s.to_str())
65 .unwrap_or("");
66 if current_ext.is_empty() {
67 output.set_extension("zfs");
68 } else {
69 output.set_extension(format!("{}.zfs", current_ext));
70 }
71 output
72 });
73
74 // Check if output exists
75 if output_path.exists() {
76 anyhow::bail!("Output file already exists: {:?}. Remove it first or specify a different output.", output_path);
77 }
78
79 println!("Encrypting: {}", display_name);
80 println!(" Size: {}", format_size(file_size, BINARY));
81 println!(" Output: {:?}", output_path);
82
83 // Get password
84 let password = if let Some(ref pw) = self.password {
85 pw.clone()
86 } else {
87 let theme = ColorfulTheme::default();
88 Password::with_theme(&theme)
89 .with_prompt("Enter password for encryption")
90 .interact()
91 .context("Failed to read password")?
92 };
93
94 // Validate password strength
95 if password.len() < 8 {
96 anyhow::bail!("Password must be at least 8 characters long");
97 }
98
99 // Create progress bar
100 let progress = if self.progress {
101 let pb = ProgressBar::new(file_size);
102 pb.set_style(
103 ProgressStyle::default_bar()
104 .template("[{elapsed_precise}] {bar:40.cyan/blue} {percent}% {bytes}/{total_bytes} ETA: {eta}")
105 .unwrap()
106 .progress_chars("##-")
107 );
108 Some(pb)
109 } else {
110 None
111 };
112
113 // Read file data
114 let file_data = tokio::fs::read(&self.input).await
115 .with_context(|| format!("Failed to read file: {:?}", self.input))?;
116
117 // Initialize crypto system
118 let crypto = create_crypto_system(&password, self.chunk_size)?;
119
120 // Encrypt file
121 let encrypted_data = crypto.encrypt_file(&file_data)
122 .context("Failed to encrypt file")?;
123
124 // Convert to CLI format
125 let encrypted_segments: Vec<EncryptedSegment> = encrypted_data
126 .into_iter()
127 .map(|data| data.into())
128 .collect();
129
130 // Generate content verification
131 let content_id = crypto.content_id(&file_data);
132
133 // Create encrypted file format
134 let encrypted_file = EncryptedFileFormat {
135 version: 1,
136 filename: display_name.to_string(),
137 original_size: file_size,
138 chunk_size_mb: self.chunk_size,
139 content_id: content_id.to_hex(),
140 hash_algorithm: self.hash.clone(),
141 encrypted_segments,
142 created_at: chrono::Utc::now(),
143 };
144
145 // Serialize and write encrypted file
146 let serialized = serde_json::to_vec_pretty(&encrypted_file)
147 .context("Failed to serialize encrypted file")?;
148
149 tokio::fs::write(&output_path, &serialized).await
150 .with_context(|| format!("Failed to write encrypted file: {:?}", output_path))?;
151
152 if let Some(pb) = progress {
153 pb.finish_with_message("Encryption complete");
154 }
155
156 println!("✅ File encrypted successfully!");
157 println!(" Encrypted file: {:?}", output_path);
158 println!(" Content ID: {}", encrypted_file.content_id);
159 println!(" Segments: {}", encrypted_file.encrypted_segments.len());
160
161 // Security reminder
162 println!();
163 println!("🔒 Security Notes:");
164 println!(" • Keep your password safe - it cannot be recovered");
165 println!(" • The encrypted file contains no plaintext metadata");
166 println!(" • Use 'zephyrfs decrypt' to restore the original file");
167
168 Ok(())
169 }
170 }
171
172 // Encrypted file format for storage
173 #[derive(Debug, serde::Serialize, serde::Deserialize)]
174 struct EncryptedFileFormat {
175 version: u32,
176 filename: String,
177 original_size: u64,
178 chunk_size_mb: u32,
179 content_id: String,
180 hash_algorithm: String,
181 encrypted_segments: Vec<EncryptedSegment>,
182 created_at: chrono::DateTime<chrono::Utc>,
183 }
184
185 #[derive(Debug, serde::Serialize, serde::Deserialize)]
186 struct EncryptedSegment {
187 segment_index: u64,
188 ciphertext: Vec<u8>,
189 nonce: [u8; 12],
190 aad: Vec<u8>,
191 key_path: Vec<u32>,
192 }
193
194 // Helper function to create crypto system
195 fn create_crypto_system(password: &str, chunk_size_mb: u32) -> Result<ZephyrCrypto> {
196 use zephyrfs_node::crypto::{ZephyrCrypto, CryptoParams, ScryptParams, AesParams, HashParams, ContentHasher, VerificationHasher};
197
198 // Create crypto parameters
199 let params = CryptoParams {
200 scrypt_params: ScryptParams {
201 log_n: 17, // Strong security
202 r: 8,
203 p: 1,
204 output_len: 64,
205 },
206 aes_params: AesParams {
207 key_len: 32,
208 nonce_len: 12,
209 tag_len: 16,
210 },
211 hash_params: HashParams {
212 content_hasher: ContentHasher::Blake3,
213 verification_hasher: VerificationHasher::Blake3,
214 },
215 };
216
217 let mut crypto = ZephyrCrypto::with_params(params);
218 crypto.init_from_password(password)
219 .context("Failed to initialize crypto system with password")?;
220
221 Ok(crypto)
222 }
223
224 // Convert between zephyrfs_node types and CLI types
225 impl From<zephyrfs_node::crypto::EncryptedData> for EncryptedSegment {
226 fn from(data: zephyrfs_node::crypto::EncryptedData) -> Self {
227 Self {
228 segment_index: data.segment_index,
229 ciphertext: data.ciphertext,
230 nonce: data.nonce,
231 aad: data.aad,
232 key_path: data.key_path,
233 }
234 }
235 }
236
237 impl From<EncryptedSegment> for zephyrfs_node::crypto::EncryptedData {
238 fn from(segment: EncryptedSegment) -> Self {
239 Self {
240 segment_index: segment.segment_index,
241 ciphertext: segment.ciphertext,
242 nonce: segment.nonce,
243 aad: segment.aad,
244 key_path: segment.key_path,
245 }
246 }
247 }
248
249 // Import necessary items from zephyrfs_node
250 use zephyrfs_node::crypto::ZephyrCrypto;