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