Skip to content

Commit d069650

Browse files
committed
feat: add option to force encryption
1 parent 32e4a74 commit d069650

15 files changed

Lines changed: 179 additions & 23 deletions

File tree

deltachat-ffi/deltachat.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,7 @@ char* dc_get_blobdir (const dc_context_t* context);
487487
* 0 = Everybody (except explicitly blocked contacts),
488488
* 1 = Contacts (default, does not include contact requests),
489489
* 2 = Nobody (calls never result in a notification).
490+
* - `force_encryption` = 1 (default) to force encryption, 0 to allow unencrypted messages.
490491
*
491492
* Also, there are configs that are only needed
492493
* if you want to use the deprecated dc_configure() API, such as:

deltachat-rpc-client/tests/test_chatlist_events.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ def test_delivery_status_failed(acfactory: ACFactory) -> None:
8787
Test change status on chatlistitem when status changes failed
8888
"""
8989
(alice,) = acfactory.get_online_accounts(1)
90+
alice.set_config("force_encryption", "0")
9091

9192
invalid_contact = alice.create_contact("example@example.com", "invalid address")
9293
invalid_chat = alice.get_chat_by_id(alice._rpc.create_chat_by_contact_id(alice.id, invalid_contact.id))

python/tests/test_3_offline.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,8 @@ def test_get_contacts_and_delete(self, acfactory):
153153

154154
def test_delete_referenced_contact_hides_contact(self, acfactory):
155155
ac1 = acfactory.get_pseudo_configured_account()
156-
contact1 = ac1.create_contact("some1@example.com", name="some1")
156+
ac2 = acfactory.get_pseudo_configured_account()
157+
contact1 = ac1.create_contact(ac2)
157158
msg = contact1.create_chat().send_text("one message")
158159
assert ac1.delete_contact(contact1)
159160
assert not msg.filemime
@@ -185,8 +186,9 @@ def ac1(self, acfactory):
185186
return acfactory.get_pseudo_configured_account()
186187

187188
@pytest.fixture()
188-
def chat1(self, ac1):
189-
return ac1.create_contact("some1@example.org", name="some1").create_chat()
189+
def chat1(self, ac1, acfactory):
190+
ac2 = acfactory.get_pseudo_configured_account()
191+
return ac1.create_contact(ac2).create_chat()
190192

191193
def test_display(self, chat1):
192194
str(chat1)
@@ -404,7 +406,7 @@ def test_create_contact(self, acfactory):
404406
contact2 = ac1.create_contact("display1 <x@example.org>", "real")
405407
assert contact2.name == "real"
406408

407-
def test_send_lots_of_offline_msgs(self, acfactory):
409+
def test_send_lots_of_offline_msgs(self, acfactory, chat1):
408410
ac1 = acfactory.get_pseudo_configured_account()
409411
ac1.set_config("configured_mail_server", "example.org")
410412
ac1.set_config("configured_mail_user", "example.org")
@@ -413,13 +415,13 @@ def test_send_lots_of_offline_msgs(self, acfactory):
413415
ac1.set_config("configured_send_user", "example.org")
414416
ac1.set_config("configured_send_pw", "example.org")
415417
ac1.start_io()
416-
chat = ac1.create_contact("some1@example.org", name="some1").create_chat()
417418
for i in range(50):
418-
chat.send_text("hello")
419+
chat1.send_text("hello")
419420

420421
def test_create_chat_simple(self, acfactory):
421422
ac1 = acfactory.get_pseudo_configured_account()
422-
contact1 = ac1.create_contact("some1@example.org", name="some1")
423+
ac2 = acfactory.get_pseudo_configured_account()
424+
contact1 = ac1.create_contact(ac2)
423425
contact1.create_chat().send_text("hello")
424426

425427
def test_chat_message_distinctions(self, ac1, chat1):

python/tests/test_4_lowlevel.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,8 @@ def test_sig():
152152

153153
def test_markseen_invalid_message_ids(acfactory):
154154
ac1 = acfactory.get_pseudo_configured_account()
155-
contact1 = ac1.create_contact("some1@example.com", name="some1")
155+
ac2 = acfactory.get_pseudo_configured_account()
156+
contact1 = ac1.create_contact(ac2)
156157
chat = contact1.create_chat()
157158
chat.send_text("one message")
158159
ac1._evtracker.get_matching("DC_EVENT_MSGS_CHANGED")

src/chat.rs

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2823,7 +2823,12 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -
28232823
.await?;
28242824
}
28252825

2826-
let needs_encryption = msg.param.get_bool(Param::GuaranteeE2ee).unwrap_or_default();
2826+
let needs_encryption = msg.param.get_bool(Param::GuaranteeE2ee).unwrap_or_default()
2827+
|| (!msg
2828+
.param
2829+
.get_bool(Param::ForcePlaintext)
2830+
.unwrap_or_default()
2831+
&& context.get_config_bool(Config::ForceEncryption).await?);
28272832
let mimefactory = match MimeFactory::from_msg(context, msg.clone()).await {
28282833
Ok(mf) => mf,
28292834
Err(err) => {
@@ -2890,11 +2895,26 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -
28902895
}
28912896

28922897
if needs_encryption && !rendered_msg.is_encrypted {
2893-
/* unrecoverable */
2894-
message::set_msg_failed(
2898+
let addr = context.get_config(Config::ConfiguredAddr).await?;
2899+
let text = stock_str::unencrypted_email(
2900+
context,
2901+
addr.unwrap_or_default()
2902+
.split('@')
2903+
.nth(1)
2904+
.unwrap_or_default(),
2905+
)
2906+
.await;
2907+
message::set_msg_failed(context, msg, &text).await?;
2908+
add_info_msg_with_cmd(
28952909
context,
2896-
msg,
2897-
"End-to-end-encryption unavailable unexpectedly.",
2910+
msg.chat_id,
2911+
&text,
2912+
SystemMessage::InvalidUnencryptedMail,
2913+
Some(msg.timestamp_sort),
2914+
msg.timestamp_sort,
2915+
None,
2916+
None,
2917+
None,
28982918
)
28992919
.await?;
29002920
bail!(

src/config.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -486,6 +486,13 @@ pub enum Config {
486486
/// Experimental option denoting that the current profile is shared between multiple team members.
487487
/// For now, the only effect of this option is that seen flags are not synchronized.
488488
TeamProfile,
489+
490+
/// Force encryption.
491+
///
492+
/// When enabled, unencrypted messages cannot be sent
493+
/// and incoming unencrypted messages are not fetched and not processed.
494+
#[strum(props(default = "1"))]
495+
ForceEncryption,
489496
}
490497

491498
impl Config {
@@ -501,7 +508,11 @@ impl Config {
501508
pub(crate) fn is_synced(&self) -> bool {
502509
matches!(
503510
self,
504-
Self::Displayname | Self::MdnsEnabled | Self::Selfavatar | Self::Selfstatus,
511+
Self::Displayname
512+
| Self::MdnsEnabled
513+
| Self::Selfavatar
514+
| Self::Selfstatus
515+
| Self::ForceEncryption,
505516
)
506517
}
507518

src/context.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1056,6 +1056,12 @@ impl Context {
10561056
"team_profile",
10571057
self.get_config_bool(Config::TeamProfile).await?.to_string(),
10581058
);
1059+
res.insert(
1060+
"force_encryption",
1061+
self.get_config_bool(Config::ForceEncryption)
1062+
.await?
1063+
.to_string(),
1064+
);
10591065

10601066
let elapsed = time_elapsed(&self.creation_time);
10611067
res.insert("uptime", duration_to_str(elapsed));

src/e2ee.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,9 +109,11 @@ pub async fn ensure_secret_key_exists(context: &Context) -> Result<()> {
109109
#[cfg(test)]
110110
mod tests {
111111
use super::*;
112+
use crate::chat;
112113
use crate::chat::send_text_msg;
113114
use crate::config::Config;
114115
use crate::message::Message;
116+
use crate::mimeparser::SystemMessage;
115117
use crate::receive_imf::receive_imf;
116118
use crate::test_utils::{TestContext, TestContextManager};
117119

@@ -155,6 +157,28 @@ Sent with my Delta Chat Messenger: https://delta.chat";
155157
);
156158
}
157159

160+
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
161+
async fn test_cannot_send_unencrypted_by_default() -> Result<()> {
162+
let mut tcm = TestContextManager::new();
163+
let alice = &tcm.alice().await;
164+
let bob = &tcm.bob().await;
165+
let chat = alice.create_email_chat(bob).await;
166+
167+
let mut msg = Message::new_text("Hello!".to_string());
168+
assert!(chat::send_msg(alice, chat.id, &mut msg).await.is_err());
169+
assert_eq!(
170+
msg.error().unwrap(),
171+
"\u{26a0}\u{fe0f} Your email provider example.org requires end-to-end encryption which is not setup yet."
172+
);
173+
let info_msg = alice.get_last_msg().await;
174+
assert_eq!(
175+
info_msg.get_info_type(),
176+
SystemMessage::InvalidUnencryptedMail
177+
);
178+
179+
Ok(())
180+
}
181+
158182
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
159183
async fn test_chatmail_can_send_unencrypted() -> Result<()> {
160184
let mut tcm = TestContextManager::new();

src/imap.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1994,12 +1994,26 @@ pub(crate) async fn prefetch_should_download(
19941994
// prevent_rename=true as this might be a mailing list message and in this case it would be bad if we rename the contact.
19951995
// (prevent_rename is the last argument of from_field_to_contact_id())
19961996

1997+
// New SecureJoin is fully encrypted,
1998+
// but for compatibility we still download legacy `Secure-Join: vc-request` messages.
1999+
let is_legacy_securejoin = headers.get_header_value(HeaderDef::SecureJoin).is_some();
2000+
2001+
let is_encrypted = headers
2002+
.get_header_value(HeaderDef::ContentType)
2003+
.is_some_and(|content_type| {
2004+
mailparse::parse_content_type(&content_type).mimetype == "multipart/encrypted"
2005+
});
2006+
19972007
if flags.any(|f| f == Flag::Draft) {
19982008
info!(context, "Ignoring draft message");
19992009
return Ok(false);
20002010
}
20012011

2002-
let should_download = !blocked_contact || maybe_ndn;
2012+
let should_download = maybe_ndn
2013+
|| (!blocked_contact
2014+
&& (is_legacy_securejoin
2015+
|| is_encrypted
2016+
|| !context.get_config_bool(Config::ForceEncryption).await?));
20032017
Ok(should_download)
20042018
}
20052019

src/imap/session.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ const PREFETCH_FLAGS: &str = "(UID RFC822.SIZE BODY.PEEK[HEADER.FIELDS (\
2121
DATE \
2222
X-MICROSOFT-ORIGINAL-MESSAGE-ID \
2323
FROM \
24+
CONTENT-TYPE \
25+
SECURE-JOIN \
2426
CHAT-VERSION \
2527
CHAT-IS-POST-MESSAGE \
2628
AUTOCRYPT-SETUP-MESSAGE\

0 commit comments

Comments
 (0)