backup.rs
1 use std::cmp::Reverse; 2 use std::collections::{BTreeMap, BTreeSet}; 3 use std::io::{Cursor, Error, Read, Write}; 4 5 use anyhow::{bail, Context, Result}; 6 use fedimint_api_client::api::DynGlobalApi; 7 use fedimint_core::core::backup::{ 8 BackupRequest, SignedBackupRequest, BACKUP_REQUEST_MAX_PAYLOAD_SIZE_BYTES, 9 }; 10 use fedimint_core::core::ModuleInstanceId; 11 use fedimint_core::db::IDatabaseTransactionOpsCoreTyped; 12 use fedimint_core::encoding::{Decodable, DecodeError, Encodable}; 13 use fedimint_core::module::registry::ModuleDecoderRegistry; 14 use fedimint_derive_secret::DerivableSecret; 15 use fedimint_logging::{LOG_CLIENT, LOG_CLIENT_BACKUP, LOG_CLIENT_RECOVERY}; 16 use secp256k1_zkp::{KeyPair, Secp256k1}; 17 use serde::{Deserialize, Serialize}; 18 use tracing::{debug, info, warn}; 19 20 use super::Client; 21 use crate::db::LastBackupKey; 22 use crate::get_decoded_client_secret; 23 use crate::module::recovery::DynModuleBackup; 24 use crate::secret::DeriveableSecretClientExt; 25 26 /// Backup metadata 27 /// 28 /// A backup can have a blob of extra data encoded in it. We provide methods to 29 /// use json encoding, but clients are free to use their own encoding. 30 #[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Encodable, Decodable, Clone)] 31 pub struct Metadata(Vec<u8>); 32 33 impl Metadata { 34 /// Create empty metadata 35 pub fn empty() -> Self { 36 Self(vec![]) 37 } 38 39 pub fn from_raw(bytes: Vec<u8>) -> Self { 40 Self(bytes) 41 } 42 43 pub fn into_raw(self) -> Vec<u8> { 44 self.0 45 } 46 47 /// Is metadata empty 48 pub fn is_empty(&self) -> bool { 49 self.0.is_empty() 50 } 51 52 /// Create metadata as json from typed `val` 53 pub fn from_json_serialized<T: Serialize>(val: T) -> Self { 54 Self(serde_json::to_vec(&val).expect("serializing to vec can't fail")) 55 } 56 57 /// Attempt to deserialize metadata as typed json 58 pub fn to_json_deserialized<T: serde::de::DeserializeOwned>(&self) -> Result<T> { 59 Ok(serde_json::from_slice(&self.0)?) 60 } 61 62 /// Attempt to deserialize metadata as untyped json (`serde_json::Value`) 63 pub fn to_json_value(&self) -> Result<serde_json::Value> { 64 Ok(serde_json::from_slice(&self.0)?) 65 } 66 } 67 68 /// Client state backup 69 #[derive(PartialEq, Eq, Debug, Clone)] 70 pub struct ClientBackup { 71 /// Session count taken right before taking the backup 72 /// used to timestamp the backup file. Used for finding the 73 /// most recent backup from all available ones. 74 /// 75 /// Warning: Each particular module backup for each instance 76 /// in `Self::modules` could have been taken earlier than 77 /// that (e.g. older one used due to size limits), so modules 78 /// MUST maintain their own `session_count`s. 79 pub session_count: u64, 80 /// Application metadata 81 pub metadata: Metadata, 82 // TODO: remove redundant ModuleInstanceId 83 /// Module specific-backup (if supported) 84 pub modules: BTreeMap<ModuleInstanceId, DynModuleBackup>, 85 } 86 87 impl ClientBackup { 88 pub const PADDING_ALIGNMENT: usize = 4 * 1024; 89 90 /// "32kiB is enough for any module backup" --dpc 91 /// 92 /// Federation storage is scarce, and since we can take older versions of 93 /// the backup, temporarily going over the limit is not a big problem. 94 pub const PER_MODULE_SIZE_LIMIT_BYTES: usize = 32 * 1024; 95 96 /// Align an ecoded message size up for better privacy 97 fn get_alignment_size(len: usize) -> usize { 98 let padding_alignment = Self::PADDING_ALIGNMENT; 99 ((len.saturating_sub(1) / padding_alignment) + 1) * padding_alignment 100 } 101 102 /// Encrypt with a key and turn into [`EncryptedClientBackup`] 103 pub fn encrypt_to(&self, key: &fedimint_aead::LessSafeKey) -> Result<EncryptedClientBackup> { 104 let encoded = Encodable::consensus_encode_to_vec(self); 105 106 let encrypted = fedimint_aead::encrypt(encoded, key)?; 107 Ok(EncryptedClientBackup(encrypted)) 108 } 109 110 /// Validate and fallback invalid parts of the backup 111 /// 112 /// Given the size constraints and possible 3rd party modules, 113 /// it seems to use older, but smaller versions of backups when 114 /// current ones do not fit (either globally or in per-module limit). 115 fn validate_and_fallback_module_backups( 116 self, 117 last_backup: Option<&ClientBackup>, 118 ) -> ClientBackup { 119 // take all module ids from both backup and add them together 120 let all_ids: BTreeSet<_> = self 121 .modules 122 .keys() 123 .chain(last_backup.iter().flat_map(|b| b.modules.keys())) 124 .copied() 125 .collect(); 126 127 let mut modules = BTreeMap::new(); 128 for module_id in all_ids { 129 if let Some(module_backup) = self 130 .modules 131 .get(&module_id) 132 .or_else(|| last_backup.and_then(|lb| lb.modules.get(&module_id))) 133 { 134 let size = module_backup.consensus_encode_to_len(); 135 let limit = Self::PER_MODULE_SIZE_LIMIT_BYTES; 136 if size < limit { 137 modules.insert(module_id, module_backup.clone()); 138 } else if let Some(last_module_backup) = 139 last_backup.and_then(|lb| lb.modules.get(&module_id)) 140 { 141 let size_previous = last_module_backup.consensus_encode_to_len(); 142 warn!( 143 size, 144 limit, 145 %module_id, 146 size_previous, 147 "Module backup too large, will use previous version" 148 ); 149 modules.insert(module_id, last_module_backup.clone()); 150 } else { 151 warn!( 152 size, 153 limit, 154 %module_id, 155 "Module backup too large, no previous version available to fall-back to" 156 ); 157 } 158 } 159 } 160 ClientBackup { 161 session_count: self.session_count, 162 metadata: self.metadata, 163 modules, 164 } 165 } 166 } 167 168 impl Encodable for ClientBackup { 169 fn consensus_encode<W: Write>(&self, writer: &mut W) -> std::result::Result<usize, Error> { 170 let mut len = 0; 171 len += self.session_count.consensus_encode(writer)?; 172 len += self.metadata.consensus_encode(writer)?; 173 len += self.modules.consensus_encode(writer)?; 174 175 // FIXME: this still leaks some information about the backup size if the padding 176 // is so short that its length is encoded as 1 byte instead of 3. 177 let estimated_len = len + 3; 178 179 // Hide small changes in backup size for privacy 180 let alignment_size = Self::get_alignment_size(estimated_len); // +3 for most likely padding len len 181 let padding = vec![0u8; alignment_size - estimated_len]; 182 len += padding.consensus_encode(writer)?; 183 184 Ok(len) 185 } 186 } 187 188 impl Decodable for ClientBackup { 189 fn consensus_decode<R: Read>( 190 r: &mut R, 191 modules: &ModuleDecoderRegistry, 192 ) -> std::result::Result<Self, DecodeError> { 193 let session_count = u64::consensus_decode(r, modules).context("session_count")?; 194 let metadata = Metadata::consensus_decode(r, modules).context("metadata")?; 195 let module_backups = 196 BTreeMap::<ModuleInstanceId, DynModuleBackup>::consensus_decode(r, modules) 197 .context("module_backups")?; 198 let _padding = Vec::<u8>::consensus_decode(r, modules).context("padding")?; 199 200 Ok(Self { 201 session_count, 202 metadata, 203 modules: module_backups, 204 }) 205 } 206 } 207 208 /// Encrypted version of [`ClientBackup`]. 209 #[derive(Clone)] 210 pub struct EncryptedClientBackup(Vec<u8>); 211 212 impl EncryptedClientBackup { 213 pub fn decrypt_with( 214 mut self, 215 key: &fedimint_aead::LessSafeKey, 216 decoders: &ModuleDecoderRegistry, 217 ) -> Result<ClientBackup> { 218 let decrypted = fedimint_aead::decrypt(&mut self.0, key)?; 219 Ok(ClientBackup::consensus_decode( 220 &mut Cursor::new(decrypted), 221 decoders, 222 )?) 223 } 224 225 pub fn into_backup_request(self, keypair: &KeyPair) -> Result<SignedBackupRequest> { 226 let request = BackupRequest { 227 id: keypair.public_key(), 228 timestamp: fedimint_core::time::now(), 229 payload: self.0, 230 }; 231 232 request.sign(keypair) 233 } 234 235 pub fn len(&self) -> usize { 236 self.0.len() 237 } 238 239 #[must_use] 240 pub fn is_empty(&self) -> bool { 241 self.len() == 0 242 } 243 } 244 245 impl Client { 246 /// Create a backup, include provided `metadata` 247 pub async fn create_backup(&self, metadata: Metadata) -> anyhow::Result<ClientBackup> { 248 let session_count = self.api.session_count().await?; 249 let mut modules = BTreeMap::new(); 250 for (id, kind, module) in self.modules.iter_modules() { 251 debug!(target: LOG_CLIENT_BACKUP, module_id=id, module_kind=%kind, "Preparing module backup"); 252 if module.supports_backup() { 253 let backup = module.backup(id).await?; 254 255 info!(target: LOG_CLIENT_BACKUP, module_id=id, module_kind=%kind, "Prepared module backup"); 256 modules.insert(id, backup); 257 } else { 258 info!(target: LOG_CLIENT_BACKUP, module_id=id, module_kind=%kind, "Module does not support backup"); 259 } 260 } 261 262 Ok(ClientBackup { 263 metadata, 264 modules, 265 session_count, 266 }) 267 } 268 269 async fn load_previous_backup(&self) -> Option<ClientBackup> { 270 let mut dbtx = self.db.begin_transaction_nc().await; 271 dbtx.get_value(&LastBackupKey).await 272 } 273 274 async fn store_last_backup(&self, backup: &ClientBackup) { 275 let mut dbtx = self.db.begin_transaction().await; 276 dbtx.insert_entry(&LastBackupKey, backup).await; 277 dbtx.commit_tx().await; 278 } 279 280 /// Prepare an encrypted backup and send it to federation for storing 281 pub async fn backup_to_federation(&self, metadata: Metadata) -> Result<()> { 282 let last_backup = self.load_previous_backup().await; 283 let new_backup = self.create_backup(metadata).await?; 284 285 let new_backup = new_backup.validate_and_fallback_module_backups(last_backup.as_ref()); 286 287 let encrypted = new_backup.encrypt_to(&self.get_derived_backup_encryption_key())?; 288 289 self.validate_backup(&encrypted)?; 290 291 self.store_last_backup(&new_backup).await; 292 293 self.upload_backup(&encrypted).await?; 294 295 Ok(()) 296 } 297 298 /// Validate backup before sending it to federation 299 pub fn validate_backup(&self, backup: &EncryptedClientBackup) -> Result<()> { 300 if BACKUP_REQUEST_MAX_PAYLOAD_SIZE_BYTES < backup.len() { 301 bail!("Backup payload too large"); 302 } 303 Ok(()) 304 } 305 306 /// Upload `backup` to federation 307 pub async fn upload_backup(&self, backup: &EncryptedClientBackup) -> Result<()> { 308 self.validate_backup(backup)?; 309 let size = backup.len(); 310 info!( 311 target: LOG_CLIENT_BACKUP, 312 size, "Uploading backup to federation" 313 ); 314 let backup_request = backup 315 .clone() 316 .into_backup_request(&self.get_derived_backup_signing_key())?; 317 self.api.upload_backup(&backup_request).await?; 318 info!( 319 target: LOG_CLIENT_BACKUP, 320 size, "Uploaded backup to federation" 321 ); 322 Ok(()) 323 } 324 325 pub async fn download_backup_from_federation(&self) -> Result<Option<ClientBackup>> { 326 Self::download_backup_from_federation_static(&self.api, &self.root_secret(), &self.decoders) 327 .await 328 } 329 330 /// Download most recent valid backup found from the Federation 331 pub async fn download_backup_from_federation_static( 332 api: &DynGlobalApi, 333 root_secret: &DerivableSecret, 334 decoders: &ModuleDecoderRegistry, 335 ) -> Result<Option<ClientBackup>> { 336 debug!(target: LOG_CLIENT, "Downloading backup from the federation"); 337 let mut responses: Vec<_> = api 338 .download_backup(&Client::get_backup_id_static(root_secret)) 339 .await? 340 .into_iter() 341 .filter_map(|backup| { 342 match EncryptedClientBackup(backup.data).decrypt_with( 343 &Self::get_derived_backup_encryption_key_static(root_secret), 344 decoders, 345 ) { 346 Ok(valid) => Some(valid), 347 Err(e) => { 348 warn!( 349 target: LOG_CLIENT_RECOVERY, 350 "Invalid backup returned by one of the peers: {e}" 351 ); 352 None 353 } 354 } 355 }) 356 .collect(); 357 358 debug!( 359 target: LOG_CLIENT_RECOVERY, 360 "Received {} valid responses", 361 responses.len() 362 ); 363 // Use the newest (highest epoch) 364 responses.sort_by_key(|backup| Reverse(backup.session_count)); 365 366 Ok(responses.into_iter().next()) 367 } 368 369 /// Backup id derived from the root secret key (public key used to self-sign 370 /// backup requests) 371 pub fn get_backup_id(&self) -> bitcoin::secp256k1::PublicKey { 372 self.get_derived_backup_signing_key().public_key() 373 } 374 375 pub fn get_backup_id_static(root_secret: &DerivableSecret) -> bitcoin::secp256k1::PublicKey { 376 Self::get_derived_backup_signing_key_static(root_secret).public_key() 377 } 378 /// Static version of [`Self::get_derived_backup_encryption_key`] for 379 /// testing without creating whole `MintClient` 380 fn get_derived_backup_encryption_key_static( 381 secret: &DerivableSecret, 382 ) -> fedimint_aead::LessSafeKey { 383 fedimint_aead::LessSafeKey::new(secret.derive_backup_secret().to_chacha20_poly1305_key()) 384 } 385 386 /// Static version of [`Self::get_derived_backup_signing_key`] for testing 387 /// without creating whole `MintClient` 388 fn get_derived_backup_signing_key_static(secret: &DerivableSecret) -> KeyPair { 389 secret 390 .derive_backup_secret() 391 .to_secp_key(&Secp256k1::<secp256k1_zkp::SignOnly>::gen_new()) 392 } 393 394 fn get_derived_backup_encryption_key(&self) -> fedimint_aead::LessSafeKey { 395 Self::get_derived_backup_encryption_key_static(&self.root_secret()) 396 } 397 398 fn get_derived_backup_signing_key(&self) -> KeyPair { 399 Self::get_derived_backup_signing_key_static(&self.root_secret()) 400 } 401 402 pub async fn get_decoded_client_secret<T: Decodable>(&self) -> anyhow::Result<T> { 403 get_decoded_client_secret::<T>(self.db()).await 404 } 405 } 406 407 #[cfg(test)] 408 mod tests;