]> git.proxmox.com Git - proxmox-backup.git/blob - src/backup/manifest.rs
datastore: add manifest locking
[proxmox-backup.git] / src / backup / manifest.rs
1 use anyhow::{bail, format_err, Error};
2 use std::convert::TryFrom;
3 use std::path::Path;
4
5 use serde_json::{json, Value};
6 use ::serde::{Deserialize, Serialize};
7
8 use crate::backup::{BackupDir, CryptMode, CryptConfig};
9
10 pub const MANIFEST_BLOB_NAME: &str = "index.json.blob";
11 pub const MANIFEST_LOCK_NAME: &str = ".index.json.lck";
12 pub const CLIENT_LOG_BLOB_NAME: &str = "client.log.blob";
13
14 mod hex_csum {
15 use serde::{self, Deserialize, Serializer, Deserializer};
16
17 pub fn serialize<S>(
18 csum: &[u8; 32],
19 serializer: S,
20 ) -> Result<S::Ok, S::Error>
21 where
22 S: Serializer,
23 {
24 let s = proxmox::tools::digest_to_hex(csum);
25 serializer.serialize_str(&s)
26 }
27
28 pub fn deserialize<'de, D>(
29 deserializer: D,
30 ) -> Result<[u8; 32], D::Error>
31 where
32 D: Deserializer<'de>,
33 {
34 let s = String::deserialize(deserializer)?;
35 proxmox::tools::hex_to_digest(&s).map_err(serde::de::Error::custom)
36 }
37 }
38
39 fn crypt_mode_none() -> CryptMode { CryptMode::None }
40 fn empty_value() -> Value { json!({}) }
41
42 #[derive(Serialize, Deserialize)]
43 #[serde(rename_all="kebab-case")]
44 pub struct FileInfo {
45 pub filename: String,
46 #[serde(default="crypt_mode_none")] // to be compatible with < 0.8.0 backups
47 pub crypt_mode: CryptMode,
48 pub size: u64,
49 #[serde(with = "hex_csum")]
50 pub csum: [u8; 32],
51 }
52
53 impl FileInfo {
54
55 /// Return expected CryptMode of referenced chunks
56 ///
57 /// Encrypted Indices should only reference encrypted chunks, while signed or plain indices
58 /// should only reference plain chunks.
59 pub fn chunk_crypt_mode (&self) -> CryptMode {
60 match self.crypt_mode {
61 CryptMode::Encrypt => CryptMode::Encrypt,
62 CryptMode::SignOnly | CryptMode::None => CryptMode::None,
63 }
64 }
65 }
66
67 #[derive(Serialize, Deserialize)]
68 #[serde(rename_all="kebab-case")]
69 pub struct BackupManifest {
70 backup_type: String,
71 backup_id: String,
72 backup_time: i64,
73 files: Vec<FileInfo>,
74 #[serde(default="empty_value")] // to be compatible with < 0.8.0 backups
75 pub unprotected: Value,
76 pub signature: Option<String>,
77 }
78
79 #[derive(PartialEq)]
80 pub enum ArchiveType {
81 FixedIndex,
82 DynamicIndex,
83 Blob,
84 }
85
86 pub fn archive_type<P: AsRef<Path>>(
87 archive_name: P,
88 ) -> Result<ArchiveType, Error> {
89
90 let archive_name = archive_name.as_ref();
91 let archive_type = match archive_name.extension().and_then(|ext| ext.to_str()) {
92 Some("didx") => ArchiveType::DynamicIndex,
93 Some("fidx") => ArchiveType::FixedIndex,
94 Some("blob") => ArchiveType::Blob,
95 _ => bail!("unknown archive type: {:?}", archive_name),
96 };
97 Ok(archive_type)
98 }
99
100
101 impl BackupManifest {
102
103 pub fn new(snapshot: BackupDir) -> Self {
104 Self {
105 backup_type: snapshot.group().backup_type().into(),
106 backup_id: snapshot.group().backup_id().into(),
107 backup_time: snapshot.backup_time(),
108 files: Vec::new(),
109 unprotected: json!({}),
110 signature: None,
111 }
112 }
113
114 pub fn add_file(&mut self, filename: String, size: u64, csum: [u8; 32], crypt_mode: CryptMode) -> Result<(), Error> {
115 let _archive_type = archive_type(&filename)?; // check type
116 self.files.push(FileInfo { filename, size, csum, crypt_mode });
117 Ok(())
118 }
119
120 pub fn files(&self) -> &[FileInfo] {
121 &self.files[..]
122 }
123
124 pub fn lookup_file_info(&self, name: &str) -> Result<&FileInfo, Error> {
125
126 let info = self.files.iter().find(|item| item.filename == name);
127
128 match info {
129 None => bail!("manifest does not contain file '{}'", name),
130 Some(info) => Ok(info),
131 }
132 }
133
134 pub fn verify_file(&self, name: &str, csum: &[u8; 32], size: u64) -> Result<(), Error> {
135
136 let info = self.lookup_file_info(name)?;
137
138 if size != info.size {
139 bail!("wrong size for file '{}' ({} != {})", name, info.size, size);
140 }
141
142 if csum != &info.csum {
143 bail!("wrong checksum for file '{}'", name);
144 }
145
146 Ok(())
147 }
148
149 // Generate canonical json
150 fn to_canonical_json(value: &Value) -> Result<Vec<u8>, Error> {
151 let mut data = Vec::new();
152 Self::write_canonical_json(value, &mut data)?;
153 Ok(data)
154 }
155
156 fn write_canonical_json(value: &Value, output: &mut Vec<u8>) -> Result<(), Error> {
157 match value {
158 Value::Null => bail!("got unexpected null value"),
159 Value::String(_) | Value::Number(_) | Value::Bool(_) => {
160 serde_json::to_writer(output, &value)?;
161 }
162 Value::Array(list) => {
163 output.push(b'[');
164 let mut iter = list.iter();
165 if let Some(item) = iter.next() {
166 Self::write_canonical_json(item, output)?;
167 for item in iter {
168 output.push(b',');
169 Self::write_canonical_json(item, output)?;
170 }
171 }
172 output.push(b']');
173 }
174 Value::Object(map) => {
175 output.push(b'{');
176 let mut keys: Vec<&str> = map.keys().map(String::as_str).collect();
177 keys.sort();
178 let mut iter = keys.into_iter();
179 if let Some(key) = iter.next() {
180 serde_json::to_writer(&mut *output, &key)?;
181 output.push(b':');
182 Self::write_canonical_json(&map[key], output)?;
183 for key in iter {
184 output.push(b',');
185 serde_json::to_writer(&mut *output, &key)?;
186 output.push(b':');
187 Self::write_canonical_json(&map[key], output)?;
188 }
189 }
190 output.push(b'}');
191 }
192 }
193 Ok(())
194 }
195
196 /// Compute manifest signature
197 ///
198 /// By generating a HMAC SHA256 over the canonical json
199 /// representation, The 'unpreotected' property is excluded.
200 pub fn signature(&self, crypt_config: &CryptConfig) -> Result<[u8; 32], Error> {
201 Self::json_signature(&serde_json::to_value(&self)?, crypt_config)
202 }
203
204 fn json_signature(data: &Value, crypt_config: &CryptConfig) -> Result<[u8; 32], Error> {
205
206 let mut signed_data = data.clone();
207
208 signed_data.as_object_mut().unwrap().remove("unprotected"); // exclude
209 signed_data.as_object_mut().unwrap().remove("signature"); // exclude
210
211 let canonical = Self::to_canonical_json(&signed_data)?;
212
213 let sig = crypt_config.compute_auth_tag(&canonical);
214
215 Ok(sig)
216 }
217
218 /// Converts the Manifest into json string, and add a signature if there is a crypt_config.
219 pub fn to_string(&self, crypt_config: Option<&CryptConfig>) -> Result<String, Error> {
220
221 let mut manifest = serde_json::to_value(&self)?;
222
223 if let Some(crypt_config) = crypt_config {
224 let sig = self.signature(crypt_config)?;
225 manifest["signature"] = proxmox::tools::digest_to_hex(&sig).into();
226 }
227
228 let manifest = serde_json::to_string_pretty(&manifest).unwrap().into();
229 Ok(manifest)
230 }
231
232 /// Try to read the manifest. This verifies the signature if there is a crypt_config.
233 pub fn from_data(data: &[u8], crypt_config: Option<&CryptConfig>) -> Result<BackupManifest, Error> {
234 let json: Value = serde_json::from_slice(data)?;
235 let signature = json["signature"].as_str().map(String::from);
236
237 if let Some(ref crypt_config) = crypt_config {
238 if let Some(signature) = signature {
239 let expected_signature = proxmox::tools::digest_to_hex(&Self::json_signature(&json, crypt_config)?);
240 if signature != expected_signature {
241 bail!("wrong signature in manifest");
242 }
243 } else {
244 // not signed: warn/fail?
245 }
246 }
247
248 let manifest: BackupManifest = serde_json::from_value(json)?;
249 Ok(manifest)
250 }
251 }
252
253
254 impl TryFrom<super::DataBlob> for BackupManifest {
255 type Error = Error;
256
257 fn try_from(blob: super::DataBlob) -> Result<Self, Error> {
258 // no expected digest available
259 let data = blob.decode(None, None)
260 .map_err(|err| format_err!("decode backup manifest blob failed - {}", err))?;
261 let json: Value = serde_json::from_slice(&data[..])
262 .map_err(|err| format_err!("unable to parse backup manifest json - {}", err))?;
263 let manifest: BackupManifest = serde_json::from_value(json)?;
264 Ok(manifest)
265 }
266 }
267
268
269 #[test]
270 fn test_manifest_signature() -> Result<(), Error> {
271
272 use crate::backup::{KeyDerivationConfig};
273
274 let pw = b"test";
275
276 let kdf = KeyDerivationConfig::Scrypt {
277 n: 65536,
278 r: 8,
279 p: 1,
280 salt: Vec::new(),
281 };
282
283 let testkey = kdf.derive_key(pw)?;
284
285 let crypt_config = CryptConfig::new(testkey)?;
286
287 let snapshot: BackupDir = "host/elsa/2020-06-26T13:56:05Z".parse()?;
288
289 let mut manifest = BackupManifest::new(snapshot);
290
291 manifest.add_file("test1.img.fidx".into(), 200, [1u8; 32], CryptMode::Encrypt)?;
292 manifest.add_file("abc.blob".into(), 200, [2u8; 32], CryptMode::None)?;
293
294 manifest.unprotected["note"] = "This is not protected by the signature.".into();
295
296 let text = manifest.to_string(Some(&crypt_config))?;
297
298 let manifest: Value = serde_json::from_str(&text)?;
299 let signature = manifest["signature"].as_str().unwrap().to_string();
300
301 assert_eq!(signature, "d7b446fb7db081662081d4b40fedd858a1d6307a5aff4ecff7d5bf4fd35679e9");
302
303 let manifest: BackupManifest = serde_json::from_value(manifest)?;
304 let expected_signature = proxmox::tools::digest_to_hex(&manifest.signature(&crypt_config)?);
305
306 assert_eq!(signature, expected_signature);
307
308 Ok(())
309 }