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