diff --git a/src/blob.rs b/src/blob.rs index 40072cb53..da4d07376 100644 --- a/src/blob.rs +++ b/src/blob.rs @@ -8,6 +8,7 @@ use std::iter::FusedIterator; use std::path::{Path, PathBuf}; use anyhow::{format_err, Context as _, Result}; +use base64::Engine as _; use futures::StreamExt; use image::{DynamicImage, GenericImageView, ImageFormat, ImageOutputFormat}; use num_traits::FromPrimitive; @@ -312,6 +313,30 @@ impl<'a> BlobObject<'a> { true } + /// Returns path to the stored Base64-decoded blob. + /// + /// If `data` represents an image of known format, this adds the corresponding extension to + /// `suggested_file_stem`. + pub(crate) async fn store_from_base64( + context: &Context, + data: &str, + suggested_file_stem: &str, + ) -> Result { + let buf = base64::engine::general_purpose::STANDARD.decode(data)?; + let ext = if let Ok(format) = image::guess_format(&buf) { + if let Some(ext) = format.extensions_str().first() { + format!(".{ext}") + } else { + String::new() + } + } else { + String::new() + }; + let blob = + BlobObject::create(context, &format!("{suggested_file_stem}{ext}"), &buf).await?; + Ok(blob.as_name().to_string()) + } + pub async fn recode_to_avatar_size(&mut self, context: &Context) -> Result<()> { let blob_abs = self.to_abs_path(); diff --git a/src/config.rs b/src/config.rs index 96f633278..181346567 100644 --- a/src/config.rs +++ b/src/config.rs @@ -5,9 +5,11 @@ use std::path::Path; use std::str::FromStr; use anyhow::{ensure, Context as _, Result}; +use base64::Engine as _; use serde::{Deserialize, Serialize}; use strum::{EnumProperty, IntoEnumIterator}; use strum_macros::{AsRefStr, Display, EnumIter, EnumProperty, EnumString}; +use tokio::fs; use crate::blob::BlobObject; use crate::constants::{self, DC_VERSION_STR}; @@ -361,8 +363,8 @@ impl Config { /// /// This must be checked on both sides so that if there are different client versions, the /// synchronisation of a particular option is either done or not done in both directions. - /// Moreover, receivers of a config value need to check if a key can be synced because some - /// settings (e.g. Avatar path) could otherwise lead to exfiltration of files from a receiver's + /// Moreover, receivers of a config value need to check if a key can be synced because if it is + /// a file path, it could otherwise lead to exfiltration of files from a receiver's /// device if we assume an attacker to have control of a device in a multi-device setting or if /// multiple users are sharing an account. Another example is `Self::SyncMsgs` itself which /// mustn't be controlled by other devices. @@ -373,7 +375,7 @@ impl Config { } matches!( self, - Self::Displayname | Self::MdnsEnabled | Self::ShowEmails + Self::Displayname | Self::MdnsEnabled | Self::ShowEmails | Self::Selfavatar, ) } @@ -503,6 +505,23 @@ impl Context { } } + /// Executes [`SyncData::Config`] item sent by other device. + pub(crate) async fn sync_config(&self, key: &Config, value: &str) -> Result<()> { + let config_value; + let value = match key { + Config::Selfavatar if value.is_empty() => None, + Config::Selfavatar => { + config_value = BlobObject::store_from_base64(self, value, "avatar").await?; + Some(config_value.as_str()) + } + _ => Some(value), + }; + match key.is_synced() { + true => self.set_config_ex(Nosync, *key, value).await, + false => Ok(()), + } + } + fn check_config(key: Config, value: Option<&str>) -> Result<()> { match key { Config::Socks5Enabled @@ -565,15 +584,24 @@ impl Context { .execute("UPDATE contacts SET selfavatar_sent=0;", ()) .await?; match value { - Some(value) => { - let mut blob = BlobObject::new_from_path(self, value.as_ref()).await?; + Some(path) => { + let mut blob = BlobObject::new_from_path(self, path.as_ref()).await?; blob.recode_to_avatar_size(self).await?; self.sql .set_raw_config(key.as_ref(), Some(blob.as_name())) .await?; + if sync { + let buf = fs::read(blob.to_abs_path()).await?; + better_value = base64::engine::general_purpose::STANDARD.encode(buf); + value = Some(&better_value); + } } None => { self.sql.set_raw_config(key.as_ref(), None).await?; + if sync { + better_value = String::new(); + value = Some(&better_value); + } } } self.emit_event(EventType::SelfavatarChanged); @@ -950,6 +978,23 @@ mod tests { Some("Alice Sync".to_string()) ); + assert!(alice0.get_config(Config::Selfavatar).await?.is_none()); + let file = alice0.dir.path().join("avatar.png"); + let bytes = include_bytes!("../test-data/image/avatar64x64.png"); + tokio::fs::write(&file, bytes).await?; + alice0 + .set_config(Config::Selfavatar, Some(file.to_str().unwrap())) + .await?; + sync(&alice0, &alice1).await; + assert!(alice1 + .get_config(Config::Selfavatar) + .await? + .filter(|path| path.ends_with(".png")) + .is_some()); + alice0.set_config(Config::Selfavatar, None).await?; + sync(&alice0, &alice1).await; + assert!(alice1.get_config(Config::Selfavatar).await?.is_none()); + Ok(()) } diff --git a/src/mimeparser.rs b/src/mimeparser.rs index 69df2d423..56b1efc79 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -8,7 +8,6 @@ use std::pin::Pin; use std::str; use anyhow::{bail, Context as _, Result}; -use base64::Engine as _; use deltachat_derive::{FromSql, ToSql}; use format_flowed::unformat_flowed; use lettre_email::mime::{self, Mime}; @@ -722,37 +721,20 @@ impl MimeMessage { ) -> Option { if header_value == "0" { Some(AvatarAction::Delete) - } else if let Some(avatar) = header_value + } else if let Some(base64) = header_value .split_ascii_whitespace() .collect::() .strip_prefix("base64:") - .map(|x| base64::engine::general_purpose::STANDARD.decode(x)) { - // Avatar sent directly in the header as base64. - if let Ok(decoded_data) = avatar { - let extension = if let Ok(format) = image::guess_format(&decoded_data) { - if let Some(ext) = format.extensions_str().first() { - format!(".{ext}") - } else { - String::new() - } - } else { - String::new() - }; - match BlobObject::create(context, &format!("avatar{extension}"), &decoded_data) - .await - { - Ok(blob) => Some(AvatarAction::Change(blob.as_name().to_string())), - Err(err) => { - warn!( - context, - "Could not save decoded avatar to blob file: {:#}", err - ); - None - } + match BlobObject::store_from_base64(context, base64, "avatar").await { + Ok(path) => Some(AvatarAction::Change(path)), + Err(err) => { + warn!( + context, + "Could not decode and save avatar to blob file: {:#}", err, + ); + None } - } else { - None } } else { // Avatar sent in attachment, as previous versions of Delta Chat did. diff --git a/src/sync.rs b/src/sync.rs index b63f919f9..c43b70d68 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -276,10 +276,7 @@ impl Context { AddQrToken(token) => self.add_qr_token(token).await, DeleteQrToken(token) => self.delete_qr_token(token).await, AlterChat { id, action } => self.sync_alter_chat(id, action).await, - SyncData::Config { key, val } => match key.is_synced() { - true => self.set_config_ex(Sync::Nosync, *key, Some(val)).await, - false => Ok(()), - }, + SyncData::Config { key, val } => self.sync_config(key, val).await, }, SyncDataOrUnknown::Unknown(data) => { warn!(self, "Ignored unknown sync item: {data}.");