]> git.proxmox.com Git - proxmox-backup.git/blob - proxmox-backup-debug/src/inspect.rs
moved key_derivation.rs from pbs_datastore to pbs-config/src/key_config.rs
[proxmox-backup.git] / proxmox-backup-debug / src / inspect.rs
1 use std::collections::HashSet;
2 use std::fs::File;
3 use std::io::{Read, Seek, SeekFrom};
4 use std::path::Path;
5
6 use anyhow::{bail, format_err, Error};
7 use serde_json::{json, Value};
8 use walkdir::WalkDir;
9
10 use proxmox::api::cli::{
11 format_and_print_result, get_output_format, CliCommand, CliCommandMap, CommandLineInterface,
12 };
13 use proxmox::api::{api, cli::*};
14
15 use pbs_tools::cli::outfile_or_stdout;
16 use pbs_tools::crypt_config::CryptConfig;
17 use pbs_datastore::dynamic_index::DynamicIndexReader;
18 use pbs_datastore::file_formats::{
19 COMPRESSED_BLOB_MAGIC_1_0, DYNAMIC_SIZED_CHUNK_INDEX_1_0, ENCRYPTED_BLOB_MAGIC_1_0,
20 ENCR_COMPR_BLOB_MAGIC_1_0, FIXED_SIZED_CHUNK_INDEX_1_0, UNCOMPRESSED_BLOB_MAGIC_1_0,
21 };
22 use pbs_datastore::fixed_index::FixedIndexReader;
23 use pbs_datastore::index::IndexFile;
24 use pbs_datastore::DataBlob;
25 use pbs_config::key_config::load_and_decrypt_key;
26 use pbs_client::tools::key_source::get_encryption_key_password;
27
28
29 /// Decodes a blob and writes its content either to stdout or into a file
30 fn decode_blob(
31 mut output_path: Option<&Path>,
32 key_file: Option<&Path>,
33 digest: Option<&[u8; 32]>,
34 blob: &DataBlob,
35 ) -> Result<(), Error> {
36 let mut crypt_conf_opt = None;
37 let crypt_conf;
38
39 if blob.is_encrypted() && key_file.is_some() {
40 let (key, _created, _fingerprint) =
41 load_and_decrypt_key(&key_file.unwrap(), &get_encryption_key_password)?;
42 crypt_conf = CryptConfig::new(key)?;
43 crypt_conf_opt = Some(&crypt_conf);
44 }
45
46 output_path = match output_path {
47 Some(path) if path.eq(Path::new("-")) => None,
48 _ => output_path,
49 };
50
51 outfile_or_stdout(output_path)?.write_all(blob.decode(crypt_conf_opt, digest)?.as_slice())?;
52 Ok(())
53 }
54
55 #[api(
56 input: {
57 properties: {
58 chunk: {
59 description: "The chunk file.",
60 type: String,
61 },
62 "reference-filter": {
63 description: "Path to the directory that should be searched for references.",
64 type: String,
65 optional: true,
66 },
67 "digest": {
68 description: "Needed when searching for references, if set, it will be used for verification when decoding.",
69 type: String,
70 optional: true,
71 },
72 "decode": {
73 description: "Path to the file to which the chunk should be decoded, '-' -> decode to stdout.",
74 type: String,
75 optional: true,
76 },
77 "keyfile": {
78 description: "Path to the keyfile with which the chunk was encrypted.",
79 type: String,
80 optional: true,
81 },
82 "use-filename-as-digest": {
83 description: "The filename should be used as digest for reference search and decode verification, if no digest is specified.",
84 type: bool,
85 optional: true,
86 default: true,
87 },
88 "output-format": {
89 schema: OUTPUT_FORMAT,
90 optional: true,
91 },
92 }
93 }
94 )]
95 /// Inspect a chunk
96 fn inspect_chunk(
97 chunk: String,
98 reference_filter: Option<String>,
99 mut digest: Option<String>,
100 decode: Option<String>,
101 keyfile: Option<String>,
102 use_filename_as_digest: bool,
103 param: Value,
104 ) -> Result<(), Error> {
105 let output_format = get_output_format(&param);
106 let chunk_path = Path::new(&chunk);
107
108 if digest.is_none() && use_filename_as_digest {
109 digest = Some(if let Some((_, filename)) = chunk.rsplit_once("/") {
110 String::from(filename)
111 } else {
112 chunk.clone()
113 });
114 };
115
116 let digest_raw: Option<[u8; 32]> = digest
117 .map(|ref d| {
118 proxmox::tools::hex_to_digest(d)
119 .map_err(|e| format_err!("could not parse chunk - {}", e))
120 })
121 .map_or(Ok(None), |r| r.map(Some))?;
122
123 let search_path = reference_filter.as_ref().map(Path::new);
124 let key_file_path = keyfile.as_ref().map(Path::new);
125 let decode_output_path = decode.as_ref().map(Path::new);
126
127 let blob = DataBlob::load_from_reader(
128 &mut std::fs::File::open(&chunk_path)
129 .map_err(|e| format_err!("could not open chunk file - {}", e))?,
130 )?;
131
132 let referenced_by = if let (Some(search_path), Some(digest_raw)) = (search_path, digest_raw) {
133 let mut references = Vec::new();
134 for entry in WalkDir::new(search_path)
135 .follow_links(false)
136 .into_iter()
137 .filter_map(|e| e.ok())
138 {
139 use std::os::unix::ffi::OsStrExt;
140 let file_name = entry.file_name().as_bytes();
141
142 let index: Box<dyn IndexFile> = if file_name.ends_with(b".fidx") {
143 match FixedIndexReader::open(entry.path()) {
144 Ok(index) => Box::new(index),
145 Err(_) => continue,
146 }
147 } else if file_name.ends_with(b".didx") {
148 match DynamicIndexReader::open(entry.path()) {
149 Ok(index) => Box::new(index),
150 Err(_) => continue,
151 }
152 } else {
153 continue;
154 };
155
156 for pos in 0..index.index_count() {
157 if let Some(index_chunk_digest) = index.index_digest(pos) {
158 if digest_raw.eq(index_chunk_digest) {
159 references.push(entry.path().to_string_lossy().into_owned());
160 break;
161 }
162 }
163 }
164 }
165 if !references.is_empty() {
166 Some(references)
167 } else {
168 None
169 }
170 } else {
171 None
172 };
173
174 if decode_output_path.is_some() {
175 decode_blob(
176 decode_output_path,
177 key_file_path,
178 digest_raw.as_ref(),
179 &blob,
180 )?;
181 }
182
183 let crc_status = format!(
184 "{}({})",
185 blob.compute_crc(),
186 blob.verify_crc().map_or("BAD", |_| "OK")
187 );
188
189 let val = match referenced_by {
190 Some(references) => json!({
191 "crc": crc_status,
192 "encryption": blob.crypt_mode()?,
193 "referenced-by": references
194 }),
195 None => json!({
196 "crc": crc_status,
197 "encryption": blob.crypt_mode()?,
198 }),
199 };
200
201 if output_format == "text" {
202 println!("CRC: {}", val["crc"]);
203 println!("encryption: {}", val["encryption"]);
204 if let Some(refs) = val["referenced-by"].as_array() {
205 println!("referenced by:");
206 for reference in refs {
207 println!(" {}", reference);
208 }
209 }
210 } else {
211 format_and_print_result(&val, &output_format);
212 }
213 Ok(())
214 }
215
216 #[api(
217 input: {
218 properties: {
219 file: {
220 description: "Path to the file.",
221 type: String,
222 },
223 "decode": {
224 description: "Path to the file to which the file should be decoded, '-' -> decode to stdout.",
225 type: String,
226 optional: true,
227 },
228 "keyfile": {
229 description: "Path to the keyfile with which the file was encrypted.",
230 type: String,
231 optional: true,
232 },
233 "output-format": {
234 schema: OUTPUT_FORMAT,
235 optional: true,
236 },
237 }
238 }
239 )]
240 /// Inspect a file, for blob file without decode only the size and encryption mode is printed
241 fn inspect_file(
242 file: String,
243 decode: Option<String>,
244 keyfile: Option<String>,
245 param: Value,
246 ) -> Result<(), Error> {
247 let output_format = get_output_format(&param);
248
249 let mut file = File::open(Path::new(&file))?;
250 let mut magic = [0; 8];
251 file.read_exact(&mut magic)?;
252 file.seek(SeekFrom::Start(0))?;
253 let val = match magic {
254 UNCOMPRESSED_BLOB_MAGIC_1_0
255 | COMPRESSED_BLOB_MAGIC_1_0
256 | ENCRYPTED_BLOB_MAGIC_1_0
257 | ENCR_COMPR_BLOB_MAGIC_1_0 => {
258 let data_blob = DataBlob::load_from_reader(&mut file)?;
259 let key_file_path = keyfile.as_ref().map(Path::new);
260
261 let decode_output_path = decode.as_ref().map(Path::new);
262
263 if decode_output_path.is_some() {
264 decode_blob(decode_output_path, key_file_path, None, &data_blob)?;
265 }
266
267 let crypt_mode = data_blob.crypt_mode()?;
268 json!({
269 "encryption": crypt_mode,
270 "size": data_blob.raw_size(),
271 })
272 }
273 FIXED_SIZED_CHUNK_INDEX_1_0 | DYNAMIC_SIZED_CHUNK_INDEX_1_0 => {
274 let index: Box<dyn IndexFile> = match magic {
275 FIXED_SIZED_CHUNK_INDEX_1_0 => {
276 Box::new(FixedIndexReader::new(file)?) as Box<dyn IndexFile>
277 }
278 DYNAMIC_SIZED_CHUNK_INDEX_1_0 => {
279 Box::new(DynamicIndexReader::new(file)?) as Box<dyn IndexFile>
280 }
281 _ => bail!(format_err!("This is technically not possible")),
282 };
283
284 let mut ctime_str = index.index_ctime().to_string();
285 if let Ok(s) = proxmox::tools::time::strftime_local("%c", index.index_ctime()) {
286 ctime_str = s;
287 }
288
289 let mut chunk_digests = HashSet::new();
290
291 for pos in 0..index.index_count() {
292 let digest = index.index_digest(pos).unwrap();
293 chunk_digests.insert(proxmox::tools::digest_to_hex(digest));
294 }
295
296 json!({
297 "size": index.index_size(),
298 "ctime": ctime_str,
299 "chunk-digests": chunk_digests
300 })
301 }
302 _ => bail!(format_err!(
303 "Only .blob, .fidx and .didx files may be inspected"
304 )),
305 };
306
307 if output_format == "text" {
308 println!("size: {}", val["size"]);
309 if let Some(encryption) = val["encryption"].as_str() {
310 println!("encryption: {}", encryption);
311 }
312 if let Some(ctime) = val["ctime"].as_str() {
313 println!("creation time: {}", ctime);
314 }
315 if let Some(chunks) = val["chunk-digests"].as_array() {
316 println!("chunks:");
317 for chunk in chunks {
318 println!(" {}", chunk);
319 }
320 }
321 } else {
322 format_and_print_result(&val, &output_format);
323 }
324
325 Ok(())
326 }
327
328 pub fn inspect_commands() -> CommandLineInterface {
329 let cmd_def = CliCommandMap::new()
330 .insert(
331 "chunk",
332 CliCommand::new(&API_METHOD_INSPECT_CHUNK).arg_param(&["chunk"]),
333 )
334 .insert(
335 "file",
336 CliCommand::new(&API_METHOD_INSPECT_FILE).arg_param(&["file"]),
337 );
338
339 cmd_def.into()
340 }