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