mirror of
https://github.com/chatmail/core.git
synced 2026-05-08 09:26:29 +03:00
Broadcast-securejoin is working!!
This commit is contained in:
@@ -45,6 +45,8 @@ pub enum QrObject {
|
|||||||
/// Fingerprint of the contact key as scanned from the QR code.
|
/// Fingerprint of the contact key as scanned from the QR code.
|
||||||
fingerprint: String,
|
fingerprint: String,
|
||||||
|
|
||||||
|
authcode: String,
|
||||||
|
|
||||||
/// The secret shared between all members,
|
/// The secret shared between all members,
|
||||||
/// used to symmetrically encrypt&decrypt messages.
|
/// used to symmetrically encrypt&decrypt messages.
|
||||||
shared_secret: String,
|
shared_secret: String,
|
||||||
@@ -227,6 +229,7 @@ impl From<Qr> for QrObject {
|
|||||||
grpid,
|
grpid,
|
||||||
contact_id,
|
contact_id,
|
||||||
fingerprint,
|
fingerprint,
|
||||||
|
authcode,
|
||||||
shared_secret,
|
shared_secret,
|
||||||
} => {
|
} => {
|
||||||
let contact_id = contact_id.to_u32();
|
let contact_id = contact_id.to_u32();
|
||||||
@@ -236,6 +239,7 @@ impl From<Qr> for QrObject {
|
|||||||
grpid,
|
grpid,
|
||||||
contact_id,
|
contact_id,
|
||||||
fingerprint,
|
fingerprint,
|
||||||
|
authcode,
|
||||||
shared_secret,
|
shared_secret,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
60
src/chat.rs
60
src/chat.rs
@@ -43,9 +43,9 @@ use crate::smtp::send_msg_to_smtp;
|
|||||||
use crate::stock_str;
|
use crate::stock_str;
|
||||||
use crate::sync::{self, Sync::*, SyncData};
|
use crate::sync::{self, Sync::*, SyncData};
|
||||||
use crate::tools::{
|
use crate::tools::{
|
||||||
IsNoneOrEmpty, SystemTime, buf_compress, create_broadcast_shared_secret, create_id,
|
IsNoneOrEmpty, SystemTime, buf_compress, create_id, create_outgoing_rfc724_mid,
|
||||||
create_outgoing_rfc724_mid, create_smeared_timestamp, create_smeared_timestamps, get_abs_path,
|
create_smeared_timestamp, create_smeared_timestamps, get_abs_path, gm2local_offset,
|
||||||
gm2local_offset, smeared_time, time, truncate_msg_text,
|
smeared_time, time, truncate_msg_text,
|
||||||
};
|
};
|
||||||
use crate::webxdc::StatusUpdateSerial;
|
use crate::webxdc::StatusUpdateSerial;
|
||||||
use crate::{chatlist_events, imap};
|
use crate::{chatlist_events, imap};
|
||||||
@@ -1646,6 +1646,18 @@ impl Chat {
|
|||||||
self.typ == Chattype::Mailinglist
|
self.typ == Chattype::Mailinglist
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns true if chat is an outgoing broadcast channel.
|
||||||
|
pub fn is_out_broadcast(&self) -> bool {
|
||||||
|
self.typ == Chattype::OutBroadcast
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if the chat is a broadcast channel,
|
||||||
|
/// regardless of whether self is on the sending
|
||||||
|
/// or receiving side.
|
||||||
|
pub fn is_any_broadcast(&self) -> bool {
|
||||||
|
matches!(self.typ, Chattype::OutBroadcast | Chattype::InBroadcast)
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns None if user can send messages to this chat.
|
/// Returns None if user can send messages to this chat.
|
||||||
///
|
///
|
||||||
/// Otherwise returns a reason useful for logging.
|
/// Otherwise returns a reason useful for logging.
|
||||||
@@ -1726,7 +1738,7 @@ impl Chat {
|
|||||||
match self.typ {
|
match self.typ {
|
||||||
Chattype::Single | Chattype::OutBroadcast | Chattype::Mailinglist => Ok(true),
|
Chattype::Single | Chattype::OutBroadcast | Chattype::Mailinglist => Ok(true),
|
||||||
Chattype::Group => is_contact_in_chat(context, self.id, ContactId::SELF).await,
|
Chattype::Group => is_contact_in_chat(context, self.id, ContactId::SELF).await,
|
||||||
Chattype::InBroadcast => Ok(false),
|
Chattype::InBroadcast => Ok(true),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2916,13 +2928,18 @@ async fn prepare_send_msg(
|
|||||||
CantSendReason::ContactRequest => {
|
CantSendReason::ContactRequest => {
|
||||||
// Allow securejoin messages, they are supposed to repair the verification.
|
// Allow securejoin messages, they are supposed to repair the verification.
|
||||||
// If the chat is a contact request, let the user accept it later.
|
// If the chat is a contact request, let the user accept it later.
|
||||||
|
|
||||||
msg.param.get_cmd() == SystemMessage::SecurejoinMessage
|
msg.param.get_cmd() == SystemMessage::SecurejoinMessage
|
||||||
}
|
}
|
||||||
// Allow to send "Member removed" messages so we can leave the group/broadcast.
|
// Allow to send "Member removed" messages so we can leave the group/broadcast.
|
||||||
// Necessary checks should be made anyway before removing contact
|
// Necessary checks should be made anyway before removing contact
|
||||||
// from the chat.
|
// from the chat.
|
||||||
CantSendReason::NotAMember | CantSendReason::InBroadcast => {
|
CantSendReason::NotAMember => msg.param.get_cmd() == SystemMessage::MemberRemovedFromGroup,
|
||||||
msg.param.get_cmd() == SystemMessage::MemberRemovedFromGroup
|
CantSendReason::InBroadcast => {
|
||||||
|
matches!(
|
||||||
|
msg.param.get_cmd(),
|
||||||
|
SystemMessage::MemberRemovedFromGroup | SystemMessage::SecurejoinMessage
|
||||||
|
)
|
||||||
}
|
}
|
||||||
CantSendReason::MissingKey => msg
|
CantSendReason::MissingKey => msg
|
||||||
.param
|
.param
|
||||||
@@ -3777,7 +3794,7 @@ pub async fn create_group_ex(
|
|||||||
/// Returns the created chat's id.
|
/// Returns the created chat's id.
|
||||||
pub async fn create_broadcast(context: &Context, chat_name: String) -> Result<ChatId> {
|
pub async fn create_broadcast(context: &Context, chat_name: String) -> Result<ChatId> {
|
||||||
let grpid = create_id();
|
let grpid = create_id();
|
||||||
let secret = create_broadcast_shared_secret();
|
let secret = create_id();
|
||||||
create_broadcast_ex(context, Sync, grpid, chat_name, secret).await
|
create_broadcast_ex(context, Sync, grpid, chat_name, secret).await
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3840,6 +3857,35 @@ pub(crate) async fn create_broadcast_ex(
|
|||||||
Ok(chat_id)
|
Ok(chat_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn load_broadcast_shared_secret(
|
||||||
|
context: &Context,
|
||||||
|
chat_id: ChatId,
|
||||||
|
) -> Result<Option<String>> {
|
||||||
|
Ok(context
|
||||||
|
.sql
|
||||||
|
.query_get_value(
|
||||||
|
"SELECT secret FROM broadcasts_shared_secrets WHERE chat_id=?",
|
||||||
|
(chat_id,),
|
||||||
|
)
|
||||||
|
.await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn save_broadcast_shared_secret(
|
||||||
|
context: &Context,
|
||||||
|
chat_id: ChatId,
|
||||||
|
shared_secret: &str,
|
||||||
|
) -> Result<()> {
|
||||||
|
context
|
||||||
|
.sql
|
||||||
|
.execute(
|
||||||
|
"INSERT INTO broadcasts_shared_secrets (chat_id, secret) VALUES (?, ?)
|
||||||
|
ON CONFLICT(chat_id) DO UPDATE SET secret=excluded.chat_id",
|
||||||
|
(chat_id, shared_secret),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Set chat contacts in the `chats_contacts` table.
|
/// Set chat contacts in the `chats_contacts` table.
|
||||||
pub(crate) async fn update_chat_contacts_table(
|
pub(crate) async fn update_chat_contacts_table(
|
||||||
context: &Context,
|
context: &Context,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use crate::imex::{ImexMode, has_backup, imex};
|
|||||||
use crate::message::{MessengerMessage, delete_msgs};
|
use crate::message::{MessengerMessage, delete_msgs};
|
||||||
use crate::mimeparser::{self, MimeMessage};
|
use crate::mimeparser::{self, MimeMessage};
|
||||||
use crate::receive_imf::receive_imf;
|
use crate::receive_imf::receive_imf;
|
||||||
|
use crate::securejoin::get_securejoin_qr;
|
||||||
use crate::test_utils::{
|
use crate::test_utils::{
|
||||||
AVATAR_64x64_BYTES, AVATAR_64x64_DEDUPLICATED, E2EE_INFO_MSGS, TestContext, TestContextManager,
|
AVATAR_64x64_BYTES, AVATAR_64x64_DEDUPLICATED, E2EE_INFO_MSGS, TestContext, TestContextManager,
|
||||||
TimeShiftFalsePositiveNote, sync,
|
TimeShiftFalsePositiveNote, sync,
|
||||||
@@ -2929,11 +2930,13 @@ async fn test_broadcast_channel_protected_listid() -> Result<()> {
|
|||||||
let mut tcm = TestContextManager::new();
|
let mut tcm = TestContextManager::new();
|
||||||
let alice = &tcm.alice().await;
|
let alice = &tcm.alice().await;
|
||||||
let bob = &tcm.bob().await;
|
let bob = &tcm.bob().await;
|
||||||
let alice_bob_contact_id = alice.add_or_lookup_contact_id(bob).await;
|
|
||||||
|
|
||||||
tcm.section("Create a broadcast channel with Bob, and send a message");
|
tcm.section("Create a broadcast channel with Bob, and send a message");
|
||||||
let alice_chat_id = create_broadcast(alice, "My Channel".to_string()).await?;
|
let alice_chat_id = create_broadcast(alice, "My Channel".to_string()).await?;
|
||||||
add_contact_to_chat(alice, alice_chat_id, alice_bob_contact_id).await?;
|
|
||||||
|
let qr = get_securejoin_qr(alice, Some(alice_chat_id)).await.unwrap();
|
||||||
|
tcm.exec_securejoin_qr(bob, alice, &qr).await;
|
||||||
|
|
||||||
let mut sent = alice.send_text(alice_chat_id, "Hi somebody").await;
|
let mut sent = alice.send_text(alice_chat_id, "Hi somebody").await;
|
||||||
|
|
||||||
assert!(!sent.payload.contains("List-ID"));
|
assert!(!sent.payload.contains("List-ID"));
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ use tokio::fs;
|
|||||||
|
|
||||||
use crate::aheader::{Aheader, EncryptPreference};
|
use crate::aheader::{Aheader, EncryptPreference};
|
||||||
use crate::blob::BlobObject;
|
use crate::blob::BlobObject;
|
||||||
use crate::chat::{self, Chat};
|
use crate::chat::{self, Chat, load_broadcast_shared_secret};
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::constants::ASM_SUBJECT;
|
use crate::constants::ASM_SUBJECT;
|
||||||
use crate::constants::{Chattype, DC_FROM_HANDSHAKE};
|
use crate::constants::{Chattype, DC_FROM_HANDSHAKE};
|
||||||
@@ -231,6 +231,9 @@ impl MimeFactory {
|
|||||||
|
|
||||||
// Do not encrypt messages to mailing lists.
|
// Do not encrypt messages to mailing lists.
|
||||||
encryption_keys = None;
|
encryption_keys = None;
|
||||||
|
} else if chat.is_out_broadcast() {
|
||||||
|
// Encrypt, but only symmetrically, not with the public keys.
|
||||||
|
encryption_keys = Some(Vec::new());
|
||||||
} else {
|
} else {
|
||||||
let email_to_remove = if msg.param.get_cmd() == SystemMessage::MemberRemovedFromGroup {
|
let email_to_remove = if msg.param.get_cmd() == SystemMessage::MemberRemovedFromGroup {
|
||||||
msg.param.get(Param::Arg)
|
msg.param.get(Param::Arg)
|
||||||
@@ -563,8 +566,10 @@ impl MimeFactory {
|
|||||||
// messages are auto-sent unlike usual unencrypted messages.
|
// messages are auto-sent unlike usual unencrypted messages.
|
||||||
step == "vg-request-with-auth"
|
step == "vg-request-with-auth"
|
||||||
|| step == "vc-request-with-auth"
|
|| step == "vc-request-with-auth"
|
||||||
|
|| step == "vb-request-with-auth"
|
||||||
|| step == "vg-member-added"
|
|| step == "vg-member-added"
|
||||||
|| step == "vc-contact-confirm"
|
|| step == "vc-contact-confirm"
|
||||||
|
// TODO possibly add vb-member-added here
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1144,7 +1149,7 @@ impl MimeFactory {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let symmetric_key: Option<String> = match &self.loaded {
|
let symmetric_key: Option<String> = match &self.loaded {
|
||||||
Loaded::Message { chat, .. } if chat.typ == Chattype::OutBroadcast => {
|
Loaded::Message { chat, .. } if chat.is_any_broadcast() => {
|
||||||
// If there is no symmetric key yet
|
// If there is no symmetric key yet
|
||||||
// (because this is an old broadcast channel,
|
// (because this is an old broadcast channel,
|
||||||
// created before we had symmetric encryption),
|
// created before we had symmetric encryption),
|
||||||
@@ -1152,13 +1157,7 @@ impl MimeFactory {
|
|||||||
// Symmetric encryption exists since 2025-08;
|
// Symmetric encryption exists since 2025-08;
|
||||||
// some time after that, we can think about requiring everyone
|
// some time after that, we can think about requiring everyone
|
||||||
// to switch to symmetrically-encrypted broadcast lists.
|
// to switch to symmetrically-encrypted broadcast lists.
|
||||||
context
|
load_broadcast_shared_secret(context, chat.id).await?
|
||||||
.sql
|
|
||||||
.query_get_value(
|
|
||||||
"SELECT secret FROM broadcasts_shared_secrets WHERE chat_id=?",
|
|
||||||
(chat.id,),
|
|
||||||
)
|
|
||||||
.await?
|
|
||||||
}
|
}
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
@@ -1515,7 +1514,10 @@ impl MimeFactory {
|
|||||||
let param2 = msg.param.get(Param::Arg2).unwrap_or_default();
|
let param2 = msg.param.get(Param::Arg2).unwrap_or_default();
|
||||||
if !param2.is_empty() {
|
if !param2.is_empty() {
|
||||||
headers.push((
|
headers.push((
|
||||||
if step == "vg-request-with-auth" || step == "vc-request-with-auth" {
|
if step == "vg-request-with-auth"
|
||||||
|
|| step == "vc-request-with-auth"
|
||||||
|
|| step == "vb-request-with-auth"
|
||||||
|
{
|
||||||
"Secure-Join-Auth"
|
"Secure-Join-Auth"
|
||||||
} else {
|
} else {
|
||||||
"Secure-Join-Invitenumber"
|
"Secure-Join-Invitenumber"
|
||||||
|
|||||||
18
src/qr.rs
18
src/qr.rs
@@ -96,6 +96,8 @@ pub enum Qr {
|
|||||||
|
|
||||||
fingerprint: Fingerprint,
|
fingerprint: Fingerprint,
|
||||||
|
|
||||||
|
authcode: String,
|
||||||
|
|
||||||
shared_secret: String,
|
shared_secret: String,
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -396,7 +398,7 @@ pub fn format_backup(qr: &Qr) -> Result<String> {
|
|||||||
|
|
||||||
/// scheme: `OPENPGP4FPR:FINGERPRINT#a=ADDR&n=NAME&i=INVITENUMBER&s=AUTH`
|
/// scheme: `OPENPGP4FPR:FINGERPRINT#a=ADDR&n=NAME&i=INVITENUMBER&s=AUTH`
|
||||||
/// or: `OPENPGP4FPR:FINGERPRINT#a=ADDR&g=GROUPNAME&x=GROUPID&i=INVITENUMBER&s=AUTH`
|
/// or: `OPENPGP4FPR:FINGERPRINT#a=ADDR&g=GROUPNAME&x=GROUPID&i=INVITENUMBER&s=AUTH`
|
||||||
/// or: `OPENPGP4FPR:FINGERPRINT#a=ADDR&g=BROADCAST_NAME&x=BROADCAST_ID&b=BROADCAST_SHARED_SECRET`
|
/// or: `OPENPGP4FPR:FINGERPRINT#a=ADDR&g=BROADCAST_NAME&x=BROADCAST_ID&s=AUTH&b=BROADCAST_SHARED_SECRET`
|
||||||
/// or: `OPENPGP4FPR:FINGERPRINT#a=ADDR`
|
/// or: `OPENPGP4FPR:FINGERPRINT#a=ADDR`
|
||||||
async fn decode_openpgp(context: &Context, qr: &str) -> Result<Qr> {
|
async fn decode_openpgp(context: &Context, qr: &str) -> Result<Qr> {
|
||||||
let payload = qr
|
let payload = qr
|
||||||
@@ -474,7 +476,9 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result<Qr> {
|
|||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
if let (Some(addr), Some(invitenumber), Some(authcode)) = (&addr, invitenumber, authcode) {
|
if let (Some(addr), Some(invitenumber), Some(authcode)) =
|
||||||
|
(&addr, invitenumber, authcode.clone())
|
||||||
|
{
|
||||||
let addr = ContactAddress::new(addr)?;
|
let addr = ContactAddress::new(addr)?;
|
||||||
let (contact_id, _) = Contact::add_or_lookup_ex(
|
let (contact_id, _) = Contact::add_or_lookup_ex(
|
||||||
context,
|
context,
|
||||||
@@ -545,8 +549,13 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result<Qr> {
|
|||||||
authcode,
|
authcode,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} else if let (Some(addr), Some(broadcast_name), Some(grpid), Some(shared_secret)) =
|
} else if let (
|
||||||
(&addr, grpname, grpid, broadcast_shared_secret)
|
Some(addr),
|
||||||
|
Some(broadcast_name),
|
||||||
|
Some(grpid),
|
||||||
|
Some(authcode),
|
||||||
|
Some(shared_secret),
|
||||||
|
) = (&addr, grpname, grpid, authcode, broadcast_shared_secret)
|
||||||
{
|
{
|
||||||
// This is a broadcast channel invite link.
|
// This is a broadcast channel invite link.
|
||||||
// TODO code duplication with the previous block
|
// TODO code duplication with the previous block
|
||||||
@@ -567,6 +576,7 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result<Qr> {
|
|||||||
grpid,
|
grpid,
|
||||||
contact_id,
|
contact_id,
|
||||||
fingerprint,
|
fingerprint,
|
||||||
|
authcode,
|
||||||
shared_secret,
|
shared_secret,
|
||||||
})
|
})
|
||||||
} else if let Some(addr) = addr {
|
} else if let Some(addr) = addr {
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ use crate::securejoin::{self, handle_securejoin_handshake, observe_securejoin_on
|
|||||||
use crate::simplify;
|
use crate::simplify;
|
||||||
use crate::stock_str;
|
use crate::stock_str;
|
||||||
use crate::sync::Sync::*;
|
use crate::sync::Sync::*;
|
||||||
use crate::tools::{self, buf_compress, create_broadcast_shared_secret, remove_subject_prefix};
|
use crate::tools::{self, buf_compress, create_id, remove_subject_prefix};
|
||||||
use crate::{chatlist_events, ensure_and_debug_assert, ensure_and_debug_assert_eq, location};
|
use crate::{chatlist_events, ensure_and_debug_assert, ensure_and_debug_assert_eq, location};
|
||||||
use crate::{contact, imap};
|
use crate::{contact, imap};
|
||||||
|
|
||||||
@@ -1566,7 +1566,7 @@ async fn do_chat_assignment(
|
|||||||
} else {
|
} else {
|
||||||
let name =
|
let name =
|
||||||
compute_mailinglist_name(mailinglist_header, &listid, mime_parser);
|
compute_mailinglist_name(mailinglist_header, &listid, mime_parser);
|
||||||
let secret = create_broadcast_shared_secret();
|
let secret = create_id();
|
||||||
chat::create_broadcast_ex(context, Nosync, listid, name, secret).await?
|
chat::create_broadcast_ex(context, Nosync, listid, name, secret).await?
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,7 +4,10 @@ use anyhow::{Context as _, Error, Result, bail, ensure};
|
|||||||
use deltachat_contact_tools::ContactAddress;
|
use deltachat_contact_tools::ContactAddress;
|
||||||
use percent_encoding::{NON_ALPHANUMERIC, utf8_percent_encode};
|
use percent_encoding::{NON_ALPHANUMERIC, utf8_percent_encode};
|
||||||
|
|
||||||
use crate::chat::{self, Chat, ChatId, ChatIdBlocked, ProtectionStatus, get_chat_id_by_grpid};
|
use crate::chat::{
|
||||||
|
self, Chat, ChatId, ChatIdBlocked, ProtectionStatus, get_chat_id_by_grpid,
|
||||||
|
load_broadcast_shared_secret,
|
||||||
|
};
|
||||||
use crate::chatlist_events;
|
use crate::chatlist_events;
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::constants::{Blocked, Chattype, NON_ALPHANUMERIC_WITHOUT_DOT};
|
use crate::constants::{Blocked, Chattype, NON_ALPHANUMERIC_WITHOUT_DOT};
|
||||||
@@ -46,9 +49,9 @@ fn inviter_progress(context: &Context, contact_id: ContactId, progress: usize) {
|
|||||||
|
|
||||||
/// Generates a Secure Join QR code.
|
/// Generates a Secure Join QR code.
|
||||||
///
|
///
|
||||||
/// With `group` set to `None` this generates a setup-contact QR code, with `group` set to a
|
/// With `chat` set to `None` this generates a setup-contact QR code, with `chat` set to a
|
||||||
/// [`ChatId`] generates a join-group QR code for the given chat.
|
/// [`ChatId`] generates a join-group/join-broadcast-channel QR code for the given chat.
|
||||||
pub async fn get_securejoin_qr(context: &Context, group: Option<ChatId>) -> Result<String> {
|
pub async fn get_securejoin_qr(context: &Context, chat: Option<ChatId>) -> Result<String> {
|
||||||
/*=======================================================
|
/*=======================================================
|
||||||
==== Alice - the inviter side ====
|
==== Alice - the inviter side ====
|
||||||
==== Step 1 in "Setup verified contact" protocol ====
|
==== Step 1 in "Setup verified contact" protocol ====
|
||||||
@@ -56,12 +59,13 @@ pub async fn get_securejoin_qr(context: &Context, group: Option<ChatId>) -> Resu
|
|||||||
|
|
||||||
ensure_secret_key_exists(context).await.ok();
|
ensure_secret_key_exists(context).await.ok();
|
||||||
|
|
||||||
let chat = match group {
|
let chat = match chat {
|
||||||
Some(id) => {
|
Some(id) => {
|
||||||
let chat = Chat::load_from_db(context, id).await?;
|
let chat = Chat::load_from_db(context, id).await?;
|
||||||
ensure!(
|
ensure!(
|
||||||
chat.typ == Chattype::Group,
|
chat.typ == Chattype::Group || chat.typ == Chattype::OutBroadcast,
|
||||||
"Can't generate SecureJoin QR code for 1:1 chat {id}"
|
"Can't generate SecureJoin QR code for chat {id} of type {}",
|
||||||
|
chat.typ
|
||||||
);
|
);
|
||||||
if chat.grpid.is_empty() {
|
if chat.grpid.is_empty() {
|
||||||
let err = format!("Can't generate QR code, chat {id} is a email thread");
|
let err = format!("Can't generate QR code, chat {id} is a email thread");
|
||||||
@@ -94,9 +98,28 @@ pub async fn get_securejoin_qr(context: &Context, group: Option<ChatId>) -> Resu
|
|||||||
utf8_percent_encode(&self_name, NON_ALPHANUMERIC_WITHOUT_DOT).to_string();
|
utf8_percent_encode(&self_name, NON_ALPHANUMERIC_WITHOUT_DOT).to_string();
|
||||||
|
|
||||||
let qr = if let Some(chat) = chat {
|
let qr = if let Some(chat) = chat {
|
||||||
|
if chat.typ == Chattype::OutBroadcast {
|
||||||
|
let broadcast_name = chat.get_name();
|
||||||
|
let broadcast_name_urlencoded =
|
||||||
|
utf8_percent_encode(broadcast_name, NON_ALPHANUMERIC).to_string();
|
||||||
|
let broadcast_secret = load_broadcast_shared_secret(context, chat.id)
|
||||||
|
.await?
|
||||||
|
.context("Could not find broadcast secret")?;
|
||||||
|
|
||||||
|
format!(
|
||||||
|
"https://i.delta.chat/#{}&a={}&g={}&x={}&s={}&b={}",
|
||||||
|
fingerprint.hex(),
|
||||||
|
self_addr_urlencoded,
|
||||||
|
&broadcast_name_urlencoded,
|
||||||
|
&chat.grpid,
|
||||||
|
&auth,
|
||||||
|
broadcast_secret
|
||||||
|
)
|
||||||
|
} else {
|
||||||
// parameters used: a=g=x=i=s=
|
// parameters used: a=g=x=i=s=
|
||||||
let group_name = chat.get_name();
|
let group_name = chat.get_name();
|
||||||
let group_name_urlencoded = utf8_percent_encode(group_name, NON_ALPHANUMERIC).to_string();
|
let group_name_urlencoded =
|
||||||
|
utf8_percent_encode(group_name, NON_ALPHANUMERIC).to_string();
|
||||||
if sync_token {
|
if sync_token {
|
||||||
context
|
context
|
||||||
.sync_qr_code_tokens(Some(chat.grpid.as_str()))
|
.sync_qr_code_tokens(Some(chat.grpid.as_str()))
|
||||||
@@ -112,6 +135,7 @@ pub async fn get_securejoin_qr(context: &Context, group: Option<ChatId>) -> Resu
|
|||||||
&invitenumber,
|
&invitenumber,
|
||||||
&auth,
|
&auth,
|
||||||
)
|
)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// parameters used: a=n=i=s=
|
// parameters used: a=n=i=s=
|
||||||
if sync_token {
|
if sync_token {
|
||||||
@@ -266,9 +290,9 @@ pub(crate) async fn handle_securejoin_handshake(
|
|||||||
|
|
||||||
info!(context, "Received secure-join message {step:?}.");
|
info!(context, "Received secure-join message {step:?}.");
|
||||||
|
|
||||||
let join_vg = step.starts_with("vg-");
|
// TODO talk with link2xt about whether we need to protect against this identity-misbinding attack,
|
||||||
|
// and if so, how
|
||||||
if !matches!(step, "vg-request" | "vc-request") {
|
if !matches!(step, "vg-request" | "vc-request" | "vb-request-with-auth") {
|
||||||
let mut self_found = false;
|
let mut self_found = false;
|
||||||
let self_fingerprint = load_self_public_key(context).await?.dc_fingerprint();
|
let self_fingerprint = load_self_public_key(context).await?.dc_fingerprint();
|
||||||
for (addr, key) in &mime_message.gossiped_keys {
|
for (addr, key) in &mime_message.gossiped_keys {
|
||||||
@@ -338,7 +362,7 @@ pub(crate) async fn handle_securejoin_handshake(
|
|||||||
========================================================*/
|
========================================================*/
|
||||||
bob::handle_auth_required(context, mime_message).await
|
bob::handle_auth_required(context, mime_message).await
|
||||||
}
|
}
|
||||||
"vg-request-with-auth" | "vc-request-with-auth" => {
|
"vg-request-with-auth" | "vc-request-with-auth" | "vb-request-with-auth" => {
|
||||||
/*==========================================================
|
/*==========================================================
|
||||||
==== Alice - the inviter side ====
|
==== Alice - the inviter side ====
|
||||||
==== Steps 5+6 in "Setup verified contact" protocol ====
|
==== Steps 5+6 in "Setup verified contact" protocol ====
|
||||||
@@ -399,7 +423,7 @@ pub(crate) async fn handle_securejoin_handshake(
|
|||||||
ContactId::scaleup_origin(context, &[contact_id], Origin::SecurejoinInvited).await?;
|
ContactId::scaleup_origin(context, &[contact_id], Origin::SecurejoinInvited).await?;
|
||||||
// for setup-contact, make Alice's one-to-one chat with Bob visible
|
// for setup-contact, make Alice's one-to-one chat with Bob visible
|
||||||
// (secure-join-information are shown in the group chat)
|
// (secure-join-information are shown in the group chat)
|
||||||
if !join_vg {
|
if step.starts_with("vc-") {
|
||||||
ChatId::create_for_contact(context, contact_id).await?;
|
ChatId::create_for_contact(context, contact_id).await?;
|
||||||
}
|
}
|
||||||
context.emit_event(EventType::ContactsChanged(Some(contact_id)));
|
context.emit_event(EventType::ContactsChanged(Some(contact_id)));
|
||||||
@@ -500,6 +524,7 @@ pub(crate) async fn handle_securejoin_handshake(
|
|||||||
/// we know that we are Alice (inviter-observer)
|
/// we know that we are Alice (inviter-observer)
|
||||||
/// that just marked peer (Bob) as verified
|
/// that just marked peer (Bob) as verified
|
||||||
/// in response to correct vc-request-with-auth message.
|
/// in response to correct vc-request-with-auth message.
|
||||||
|
// TODO here I may be able to fix some multi-device things
|
||||||
pub(crate) async fn observe_securejoin_on_other_device(
|
pub(crate) async fn observe_securejoin_on_other_device(
|
||||||
context: &Context,
|
context: &Context,
|
||||||
mime_message: &MimeMessage,
|
mime_message: &MimeMessage,
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ use anyhow::{Context as _, Result};
|
|||||||
|
|
||||||
use super::HandshakeMessage;
|
use super::HandshakeMessage;
|
||||||
use super::qrinvite::QrInvite;
|
use super::qrinvite::QrInvite;
|
||||||
use crate::chat::{self, ChatId, ProtectionStatus, is_contact_in_chat};
|
use crate::chat::{
|
||||||
|
self, ChatId, ProtectionStatus, is_contact_in_chat, save_broadcast_shared_secret,
|
||||||
|
};
|
||||||
use crate::constants::{Blocked, Chattype};
|
use crate::constants::{Blocked, Chattype};
|
||||||
use crate::contact::Origin;
|
use crate::contact::Origin;
|
||||||
use crate::context::Context;
|
use crate::context::Context;
|
||||||
@@ -56,8 +58,43 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul
|
|||||||
ContactId::scaleup_origin(context, &[invite.contact_id()], Origin::SecurejoinJoined).await?;
|
ContactId::scaleup_origin(context, &[invite.contact_id()], Origin::SecurejoinJoined).await?;
|
||||||
context.emit_event(EventType::ContactsChanged(None));
|
context.emit_event(EventType::ContactsChanged(None));
|
||||||
|
|
||||||
// Now start the protocol and initialise the state.
|
if let QrInvite::Broadcast { shared_secret, .. } = &invite {
|
||||||
{
|
// TODO this causes some performance penalty because joining_chat_id is used again below,
|
||||||
|
// but maybe it's fine
|
||||||
|
let broadcast_chat_id = joining_chat_id(context, &invite, chat_id).await?;
|
||||||
|
// TODO save the secret to the second device
|
||||||
|
save_broadcast_shared_secret(context, broadcast_chat_id, shared_secret).await?;
|
||||||
|
|
||||||
|
if verify_sender_by_fingerprint(context, invite.fingerprint(), invite.contact_id()).await? {
|
||||||
|
info!(context, "Using fast securejoin with symmetric encryption");
|
||||||
|
|
||||||
|
// The message has to be sent into the broadcast chat, rather than the 1:1 chat,
|
||||||
|
// so that it will be symmetrically encrypted
|
||||||
|
send_handshake_message(
|
||||||
|
context,
|
||||||
|
&invite,
|
||||||
|
broadcast_chat_id,
|
||||||
|
BobHandshakeMsg::RequestWithAuth,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Mark 1:1 chat as verified already.
|
||||||
|
chat_id
|
||||||
|
.set_protection(
|
||||||
|
context,
|
||||||
|
ProtectionStatus::Protected,
|
||||||
|
time(),
|
||||||
|
Some(invite.contact_id()),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
context.emit_event(EventType::SecurejoinJoinerProgress {
|
||||||
|
contact_id: invite.contact_id(),
|
||||||
|
progress: JoinerProgress::RequestWithAuthSent.to_usize(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Start the original (non-broadcast) protocol and initialise the state.
|
||||||
let has_key = context
|
let has_key = context
|
||||||
.sql
|
.sql
|
||||||
.exists(
|
.exists(
|
||||||
@@ -115,22 +152,22 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul
|
|||||||
Ok(group_chat_id)
|
Ok(group_chat_id)
|
||||||
}
|
}
|
||||||
QrInvite::Broadcast { .. } => {
|
QrInvite::Broadcast { .. } => {
|
||||||
// For a secure-join we need to create the group and add the contact. The group will
|
// TODO code duplication with previous block
|
||||||
// only become usable once the protocol is finished.
|
let broadcast_chat_id = joining_chat_id(context, &invite, chat_id).await?;
|
||||||
let group_chat_id = joining_chat_id(context, &invite, chat_id).await?;
|
if !is_contact_in_chat(context, broadcast_chat_id, invite.contact_id()).await? {
|
||||||
if !is_contact_in_chat(context, group_chat_id, invite.contact_id()).await? {
|
|
||||||
chat::add_to_chat_contacts_table(
|
chat::add_to_chat_contacts_table(
|
||||||
context,
|
context,
|
||||||
time(),
|
time(),
|
||||||
group_chat_id,
|
broadcast_chat_id,
|
||||||
&[invite.contact_id()],
|
&[invite.contact_id()],
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO this message should be translatable:
|
// TODO this message should be translatable:
|
||||||
let msg = "You were invited to join this channel. Waiting for the channel owner's device to reply…";
|
let msg = "You were invited to join this channel. Waiting for the channel owner's device to reply…";
|
||||||
chat::add_info_msg(context, group_chat_id, msg, time()).await?;
|
chat::add_info_msg(context, broadcast_chat_id, msg, time()).await?;
|
||||||
Ok(group_chat_id)
|
Ok(broadcast_chat_id)
|
||||||
}
|
}
|
||||||
QrInvite::Contact { .. } => {
|
QrInvite::Contact { .. } => {
|
||||||
// For setup-contact the BobState already ensured the 1:1 chat exists because it
|
// For setup-contact the BobState already ensured the 1:1 chat exists because it
|
||||||
@@ -318,7 +355,7 @@ pub(crate) async fn send_handshake_message(
|
|||||||
pub(crate) enum BobHandshakeMsg {
|
pub(crate) enum BobHandshakeMsg {
|
||||||
/// vc-request or vg-request
|
/// vc-request or vg-request
|
||||||
Request,
|
Request,
|
||||||
/// vc-request-with-auth or vg-request-with-auth
|
/// vc-request-with-auth, vg-request-with-auth, or vb-request-with-auth
|
||||||
RequestWithAuth,
|
RequestWithAuth,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -342,14 +379,14 @@ impl BobHandshakeMsg {
|
|||||||
Self::Request => match invite {
|
Self::Request => match invite {
|
||||||
QrInvite::Contact { .. } => "vc-request",
|
QrInvite::Contact { .. } => "vc-request",
|
||||||
QrInvite::Group { .. } => "vg-request",
|
QrInvite::Group { .. } => "vg-request",
|
||||||
QrInvite::Broadcast { .. } => "broadcast-request",
|
QrInvite::Broadcast { .. } => {
|
||||||
|
panic!("There is no request-with-auth for broadcasts")
|
||||||
|
} // TODO remove panic
|
||||||
},
|
},
|
||||||
Self::RequestWithAuth => match invite {
|
Self::RequestWithAuth => match invite {
|
||||||
QrInvite::Contact { .. } => "vc-request-with-auth",
|
QrInvite::Contact { .. } => "vc-request-with-auth",
|
||||||
QrInvite::Group { .. } => "vg-request-with-auth",
|
QrInvite::Group { .. } => "vg-request-with-auth",
|
||||||
QrInvite::Broadcast { .. } => {
|
QrInvite::Broadcast { .. } => "vb-request-with-auth",
|
||||||
panic!("There is no request-with-auth for broadcasts")
|
|
||||||
} // TODO remove panic
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,10 +30,11 @@ pub enum QrInvite {
|
|||||||
authcode: String,
|
authcode: String,
|
||||||
},
|
},
|
||||||
Broadcast {
|
Broadcast {
|
||||||
broadcast_name: String,
|
|
||||||
grpid: String,
|
|
||||||
contact_id: ContactId,
|
contact_id: ContactId,
|
||||||
fingerprint: Fingerprint,
|
fingerprint: Fingerprint,
|
||||||
|
broadcast_name: String,
|
||||||
|
grpid: String,
|
||||||
|
authcode: String,
|
||||||
shared_secret: String,
|
shared_secret: String,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -71,8 +72,9 @@ impl QrInvite {
|
|||||||
/// The `AUTH` code of the setup-contact/secure-join protocol.
|
/// The `AUTH` code of the setup-contact/secure-join protocol.
|
||||||
pub fn authcode(&self) -> &str {
|
pub fn authcode(&self) -> &str {
|
||||||
match self {
|
match self {
|
||||||
Self::Contact { authcode, .. } | Self::Group { authcode, .. } => authcode,
|
Self::Contact { authcode, .. }
|
||||||
Self::Broadcast { .. } => panic!("broadcast invite has no authcode"), // TODO panic
|
| Self::Group { authcode, .. }
|
||||||
|
| Self::Broadcast { authcode, .. } => authcode,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -113,15 +115,17 @@ impl TryFrom<Qr> for QrInvite {
|
|||||||
grpid,
|
grpid,
|
||||||
contact_id,
|
contact_id,
|
||||||
fingerprint,
|
fingerprint,
|
||||||
|
authcode,
|
||||||
shared_secret,
|
shared_secret,
|
||||||
} => Ok(QrInvite::Broadcast {
|
} => Ok(QrInvite::Broadcast {
|
||||||
broadcast_name,
|
broadcast_name,
|
||||||
grpid,
|
grpid,
|
||||||
contact_id,
|
contact_id,
|
||||||
fingerprint,
|
fingerprint,
|
||||||
|
authcode,
|
||||||
shared_secret,
|
shared_secret,
|
||||||
}),
|
}),
|
||||||
_ => bail!("Unsupported QR type"),
|
_ => bail!("Unsupported QR type: {qr:?}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -229,15 +229,15 @@ impl TestContextManager {
|
|||||||
pub async fn exec_securejoin_qr(
|
pub async fn exec_securejoin_qr(
|
||||||
&self,
|
&self,
|
||||||
scanner: &TestContext,
|
scanner: &TestContext,
|
||||||
scanned: &TestContext,
|
inviter: &TestContext,
|
||||||
qr: &str,
|
qr: &str,
|
||||||
) -> ChatId {
|
) -> ChatId {
|
||||||
let chat_id = join_securejoin(&scanner.ctx, qr).await.unwrap();
|
let chat_id = join_securejoin(&scanner.ctx, qr).await.unwrap();
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
if let Some(sent) = scanner.pop_sent_msg_opt(Duration::ZERO).await {
|
if let Some(sent) = scanner.pop_sent_msg_opt(Duration::ZERO).await {
|
||||||
scanned.recv_msg_opt(&sent).await;
|
inviter.recv_msg_opt(&sent).await;
|
||||||
} else if let Some(sent) = scanned.pop_sent_msg_opt(Duration::ZERO).await {
|
} else if let Some(sent) = inviter.pop_sent_msg_opt(Duration::ZERO).await {
|
||||||
scanner.recv_msg_opt(&sent).await;
|
scanner.recv_msg_opt(&sent).await;
|
||||||
} else {
|
} else {
|
||||||
break;
|
break;
|
||||||
|
|||||||
19
src/tools.rs
19
src/tools.rs
@@ -300,25 +300,6 @@ pub(crate) fn create_id() -> String {
|
|||||||
base64::engine::general_purpose::URL_SAFE.encode(arr)
|
base64::engine::general_purpose::URL_SAFE.encode(arr)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generate a shared secret for a broadcast channel, consisting of 64 characters..
|
|
||||||
///
|
|
||||||
/// The string generated by this function has 384 bits of entropy
|
|
||||||
/// and is returned as 64 Base64 characters, each containing 6 bits of entropy.
|
|
||||||
/// 384 is chosen because it is sufficiently secure
|
|
||||||
/// (larger than AES-128 keys used for message encryption)
|
|
||||||
/// and divides both by 8 (byte size) and 6 (number of bits in a single Base64 character).
|
|
||||||
// TODO ask someone what a good size would be here - also, not sure whether the AES-128 thing is true
|
|
||||||
pub(crate) fn create_broadcast_shared_secret() -> String {
|
|
||||||
// ThreadRng implements CryptoRng trait and is supposed to be cryptographically secure.
|
|
||||||
let mut rng = thread_rng();
|
|
||||||
|
|
||||||
// Generate 384 random bits.
|
|
||||||
let mut arr = [0u8; 48];
|
|
||||||
rng.fill(&mut arr[..]);
|
|
||||||
|
|
||||||
base64::engine::general_purpose::URL_SAFE.encode(arr)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns true if given string is a valid ID.
|
/// Returns true if given string is a valid ID.
|
||||||
///
|
///
|
||||||
/// All IDs generated with `create_id()` should be considered valid.
|
/// All IDs generated with `create_id()` should be considered valid.
|
||||||
|
|||||||
Reference in New Issue
Block a user