non-blocking group QR joins (#2508)

* refactor: cleanup send_handshake_msg()

- rename to send_alice_handshake_msg() as used by Alice only

- remove dead code from Bob
  (Bob's code is at BobState::send_handshake_message() since some time)

- take a contact_id and not a chat_id;
  this makes things less confusing when
  info-messages are put to the final group chat

* always directly return chat-id from dc_join_securejoin()

* take care not to create a group twice

* adapt documentation

* add info-msg on group invites; add inviter directly after creation

* document existing 'joinqr' command in repl tool

* do not create empty one-to-one chats for group-joins

* refactor: cleanup fingerprint_equals_sender()

- the function takes a contact_id directly now.
  before it consumes the first contact of a one-to-one chat -
  which may be easily confused with the group-chat in creation.
  moreover, the conversion contact_id -> chat_id -> contact_id
  is unneeded overhead.

* show info-messages in destination chat for alice

* fingerprint_equals_sender() returns Err on database failure

* tweak documentation

* clarify what an 'unfinished tasks' task is.

* add regression test for create_for_contact_with_blocked()

* rename Blocked::Manually to better fitting Blocked::Yes

* tweak test_secure_join() and make sure, Alice and Bob have only on chat after a group-join
This commit is contained in:
bjoern
2021-10-26 16:34:07 +02:00
committed by GitHub
parent 63207eb681
commit 3b7b8ea0f1
12 changed files with 287 additions and 232 deletions

View File

@@ -1,17 +1,15 @@
//! Verified contact protocol implementation as [specified by countermitm project](https://countermitm.readthedocs.io/en/stable/new.html#setup-contact-protocol).
use std::convert::TryFrom;
use std::time::{Duration, Instant};
use anyhow::{anyhow, bail, Context as _, Error, Result};
use async_std::channel::Receiver;
use anyhow::{bail, Context as _, Error, Result};
use async_std::sync::Mutex;
use percent_encoding::{utf8_percent_encode, AsciiSet, NON_ALPHANUMERIC};
use crate::aheader::EncryptPreference;
use crate::chat::{self, Chat, ChatId, ChatIdBlocked};
use crate::chat::{self, is_contact_in_chat, Chat, ChatId, ChatIdBlocked, ProtectionStatus};
use crate::config::Config;
use crate::constants::{Blocked, Viewtype, DC_CONTACT_ID_LAST_SPECIAL};
use crate::constants::{Blocked, Chattype, Viewtype, DC_CONTACT_ID_LAST_SPECIAL};
use crate::contact::{Contact, Origin, VerifiedStatus};
use crate::context::Context;
use crate::dc_tools::time;
@@ -81,8 +79,8 @@ enum StartedProtocolVariant {
SetupContact,
/// The secure-join protocol, to join a group.
SecureJoin {
ongoing_receiver: Receiver<()>,
group_id: String,
group_name: String,
},
}
@@ -110,16 +108,14 @@ impl Bob {
*guard = None;
}
let variant = match invite {
QrInvite::Group { ref grpid, .. } => {
let receiver = context
.alloc_ongoing()
.await
.map_err(|_| JoinError::OngoingRunning)?;
StartedProtocolVariant::SecureJoin {
ongoing_receiver: receiver,
group_id: grpid.clone(),
}
}
QrInvite::Group {
ref grpid,
ref name,
..
} => StartedProtocolVariant::SecureJoin {
group_id: grpid.clone(),
group_name: name.clone(),
},
_ => StartedProtocolVariant::SetupContact,
};
match BobState::start_protocol(context, invite).await {
@@ -280,9 +276,7 @@ pub enum JoinError {
/// This is the start of the process for the joiner. See the module and ffi documentation
/// for more details.
///
/// When joining a group this will start an "ongoing" process and will block until the
/// process is completed, the [`ChatId`] for the new group is not known any sooner. When
/// verifying a contact this returns immediately.
/// The function returns immediately and the handshake will run in background.
pub async fn dc_join_securejoin(context: &Context, qr: &str) -> Result<ChatId, JoinError> {
securejoin(context, qr).await.map_err(|err| {
warn!(context, "Fatal joiner error: {:#}", err);
@@ -313,39 +307,34 @@ async fn securejoin(context: &Context, qr: &str) -> Result<ChatId, JoinError> {
Ok(chat_id)
}
StartedProtocolVariant::SecureJoin {
ongoing_receiver,
group_id,
group_name,
} => {
// for a group-join, wait until the protocol is finished and the group is created
ongoing_receiver
.recv()
.await
.map_err(|_| JoinError::OngoingSenderDropped)?;
// handle_securejoin_handshake() calls Context::stop_ongoing before the group
// chat is created (it is created after handle_securejoin_handshake() returns by
// dc_receive_imf()). As a hack we just wait a bit for it to appear.
// If the protocol is aborted by Bob, this timeout will also happen.
let start = Instant::now();
let chatid = loop {
{
match chat::get_chat_id_by_grpid(context, &group_id).await? {
Some((chatid, _is_protected, _blocked)) => break chatid,
None => {
if start.elapsed() > Duration::from_secs(7) {
context.free_ongoing().await;
return Err(JoinError::Other(anyhow!(
"Ongoing sender dropped (this is a bug)"
)));
}
}
}
}
async_std::task::sleep(Duration::from_millis(50)).await;
// for a group-join, also create the chat soon and let the verification run in background.
// however, the group will become usable only when the protocol has finished.
let contact_id = invite.contact_id();
let chat_id = if let Some((chat_id, _protected, _blocked)) =
chat::get_chat_id_by_grpid(context, &group_id).await?
{
chat_id.unblock(context).await?;
chat_id
} else {
ChatId::create_multiuser_record(
context,
Chattype::Group,
group_id,
group_name,
Blocked::Not,
ProtectionStatus::Unprotected, // protection is added later as needed
)
.await?
};
context.free_ongoing().await;
Ok(chatid)
if !is_contact_in_chat(context, chat_id, contact_id).await? {
chat::add_to_chat_contacts_table(context, chat_id, contact_id).await?;
}
let msg = stock_str::secure_join_started(context, contact_id).await;
chat::add_info_msg(context, chat_id, msg, time()).await?;
Ok(chat_id)
}
}
}
@@ -358,13 +347,13 @@ async fn securejoin(context: &Context, qr: &str) -> Result<ChatId, JoinError> {
#[error("Failed sending handshake message")]
pub struct SendMsgError(#[from] anyhow::Error);
async fn send_handshake_msg(
/// Send handshake message from Alice's device;
/// Bob's handshake messages are sent in `BobState::send_handshake_message()`.
async fn send_alice_handshake_msg(
context: &Context,
contact_chat_id: ChatId,
contact_id: u32,
step: &str,
param2: &str,
fingerprint: Option<Fingerprint>,
grpid: &str,
) -> Result<(), SendMsgError> {
let mut msg = Message {
viewtype: Viewtype::Text,
@@ -373,67 +362,55 @@ async fn send_handshake_msg(
..Default::default()
};
msg.param.set_cmd(SystemMessage::SecurejoinMessage);
if step.is_empty() {
msg.param.remove(Param::Arg);
} else {
msg.param.set(Param::Arg, step);
}
if !param2.is_empty() {
msg.param.set(Param::Arg2, param2);
}
msg.param.set(Param::Arg, step);
if let Some(fp) = fingerprint {
msg.param.set(Param::Arg3, fp.hex());
}
if !grpid.is_empty() {
msg.param.set(Param::Arg4, grpid);
}
if step == "vg-request" || step == "vc-request" {
msg.param.set_int(Param::ForcePlaintext, 1);
} else {
msg.param.set_int(Param::GuaranteeE2ee, 1);
}
chat::send_msg(context, contact_chat_id, &mut msg).await?;
msg.param.set_int(Param::GuaranteeE2ee, 1);
chat::send_msg(
context,
ChatIdBlocked::get_for_contact(context, contact_id, Blocked::Yes)
.await?
.id,
&mut msg,
)
.await?;
Ok(())
}
async fn chat_id_2_contact_id(context: &Context, contact_chat_id: ChatId) -> Result<u32, Error> {
if let [contact_id] = chat::get_chat_contacts(context, contact_chat_id).await?[..] {
Ok(contact_id)
} else {
Ok(0)
}
/// Get an unblocked chat that can be used for info messages.
async fn info_chat_id(context: &Context, contact_id: u32) -> Result<ChatId> {
let chat_id_blocked = ChatIdBlocked::get_for_contact(context, contact_id, Blocked::Not).await?;
Ok(chat_id_blocked.id)
}
async fn fingerprint_equals_sender(
context: &Context,
fingerprint: &Fingerprint,
contact_chat_id: ChatId,
contact_id: u32,
) -> Result<bool, Error> {
if let [contact_id] = chat::get_chat_contacts(context, contact_chat_id).await?[..] {
if let Ok(contact) = Contact::load_from_db(context, contact_id).await {
let peerstate = match Peerstate::from_addr(context, contact.get_addr()).await {
Ok(peerstate) => peerstate,
Err(err) => {
warn!(
context,
"Failed to sender peerstate for {}: {}",
contact.get_addr(),
err
);
return Ok(false);
}
};
let contact = Contact::load_from_db(context, contact_id).await?;
let peerstate = match Peerstate::from_addr(context, contact.get_addr()).await {
Ok(peerstate) => peerstate,
Err(err) => {
warn!(
context,
"Failed to sender peerstate for {}: {}",
contact.get_addr(),
err
);
return Ok(false);
}
};
if let Some(peerstate) = peerstate {
if peerstate.public_key_fingerprint.is_some()
&& fingerprint == peerstate.public_key_fingerprint.as_ref().unwrap()
{
return Ok(true);
}
}
if let Some(peerstate) = peerstate {
if peerstate.public_key_fingerprint.is_some()
&& fingerprint == peerstate.public_key_fingerprint.as_ref().unwrap()
{
return Ok(true);
}
}
Ok(false)
}
@@ -494,21 +471,6 @@ pub(crate) async fn handle_securejoin_handshake(
">>>>>>>>>>>>>>>>>>>>>>>>> secure-join message \'{}\' received", step,
);
let contact_chat_id = {
let chat = ChatIdBlocked::get_for_contact(context, contact_id, Blocked::Not)
.await
.with_context(|| {
format!(
"Failed to look up or create chat for contact {}",
contact_id
)
})?;
if chat.blocked != Blocked::Not {
chat.id.unblock(context).await?;
}
chat.id
};
let join_vg = step.starts_with("vg-");
match step.as_str() {
@@ -538,13 +500,11 @@ pub(crate) async fn handle_securejoin_handshake(
inviter_progress!(context, contact_id, 300);
// Alice -> Bob
send_handshake_msg(
send_alice_handshake_msg(
context,
contact_chat_id,
contact_id,
&format!("{}-auth-required", &step[..2]),
"",
None,
"",
)
.await?;
Ok(HandshakeMessage::Done)
@@ -557,11 +517,19 @@ pub(crate) async fn handle_securejoin_handshake(
match context.bob.state(context).await {
Some(mut bobstate) => match bobstate.handle_message(context, mime_message).await {
Some(BobHandshakeStage::Terminated(why)) => {
could_not_establish_secure_connection(context, bobstate.chat_id(), why)
.await?;
could_not_establish_secure_connection(
context,
contact_id,
bobstate.chat_id(context).await?,
why,
)
.await?;
Ok(HandshakeMessage::Done)
}
Some(_stage) => {
let msg = stock_str::secure_join_replies(context, contact_id).await;
chat::add_info_msg(context, bobstate.chat_id(context).await?, msg, time())
.await?;
joiner_progress!(context, bobstate.invite().contact_id(), 400);
Ok(HandshakeMessage::Done)
}
@@ -584,7 +552,8 @@ pub(crate) async fn handle_securejoin_handshake(
None => {
could_not_establish_secure_connection(
context,
contact_chat_id,
contact_id,
info_chat_id(context, contact_id).await?,
"Fingerprint not provided.",
)
.await?;
@@ -594,16 +563,18 @@ pub(crate) async fn handle_securejoin_handshake(
if !encrypted_and_signed(context, mime_message, Some(&fingerprint)) {
could_not_establish_secure_connection(
context,
contact_chat_id,
contact_id,
info_chat_id(context, contact_id).await?,
"Auth not encrypted.",
)
.await?;
return Ok(HandshakeMessage::Ignore);
}
if !fingerprint_equals_sender(context, &fingerprint, contact_chat_id).await? {
if !fingerprint_equals_sender(context, &fingerprint, contact_id).await? {
could_not_establish_secure_connection(
context,
contact_chat_id,
contact_id,
info_chat_id(context, contact_id).await?,
"Fingerprint mismatch on inviter-side.",
)
.await?;
@@ -616,7 +587,8 @@ pub(crate) async fn handle_securejoin_handshake(
None => {
could_not_establish_secure_connection(
context,
contact_chat_id,
contact_id,
info_chat_id(context, contact_id).await?,
"Auth not provided.",
)
.await?;
@@ -624,14 +596,20 @@ pub(crate) async fn handle_securejoin_handshake(
}
};
if !token::exists(context, token::Namespace::Auth, auth_0).await {
could_not_establish_secure_connection(context, contact_chat_id, "Auth invalid.")
.await?;
could_not_establish_secure_connection(
context,
contact_id,
info_chat_id(context, contact_id).await?,
"Auth invalid.",
)
.await?;
return Ok(HandshakeMessage::Ignore);
}
if mark_peer_as_verified(context, &fingerprint).await.is_err() {
could_not_establish_secure_connection(
context,
contact_chat_id,
contact_id,
info_chat_id(context, contact_id).await?,
"Fingerprint mismatch on inviter-side.",
)
.await?;
@@ -639,7 +617,6 @@ pub(crate) async fn handle_securejoin_handshake(
}
Contact::scaleup_origin_by_id(context, contact_id, Origin::SecurejoinInvited).await?;
info!(context, "Auth verified.",);
secure_connection_established(context, contact_chat_id).await?;
context.emit_event(EventType::ContactsChanged(Some(contact_id)));
inviter_progress!(context, contact_id, 600);
if join_vg {
@@ -655,6 +632,7 @@ pub(crate) async fn handle_securejoin_handshake(
};
match chat::get_chat_id_by_grpid(context, field_grpid).await? {
Some((group_chat_id, _, _)) => {
secure_connection_established(context, contact_id, group_chat_id).await?;
if let Err(err) =
chat::add_contact_to_chat_ex(context, group_chat_id, contact_id, true)
.await
@@ -666,13 +644,17 @@ pub(crate) async fn handle_securejoin_handshake(
}
} else {
// Alice -> Bob
send_handshake_msg(
secure_connection_established(
context,
contact_chat_id,
contact_id,
info_chat_id(context, contact_id).await?,
)
.await?;
send_alice_handshake_msg(
context,
contact_id,
"vc-contact-confirm",
"",
Some(fingerprint),
"",
)
.await?;
@@ -694,13 +676,23 @@ pub(crate) async fn handle_securejoin_handshake(
match context.bob.state(context).await {
Some(mut bobstate) => match bobstate.handle_message(context, mime_message).await {
Some(BobHandshakeStage::Terminated(why)) => {
could_not_establish_secure_connection(context, bobstate.chat_id(), why)
.await?;
could_not_establish_secure_connection(
context,
contact_id,
bobstate.chat_id(context).await?,
why,
)
.await?;
Ok(HandshakeMessage::Done)
}
Some(BobHandshakeStage::Completed) => {
// Can only be BobHandshakeStage::Completed
secure_connection_established(context, bobstate.chat_id()).await?;
secure_connection_established(
context,
contact_id,
bobstate.chat_id(context).await?,
)
.await?;
Ok(retval)
}
Some(_) => {
@@ -784,21 +776,6 @@ pub(crate) async fn observe_securejoin_on_other_device(
.context("Not a Secure-Join message")?;
info!(context, "observing secure-join message \'{}\'", step);
let contact_chat_id = {
let chat = ChatIdBlocked::get_for_contact(context, contact_id, Blocked::Not)
.await
.with_context(|| {
format!(
"Failed to look up or create chat for contact {}",
contact_id
)
})?;
if chat.blocked != Blocked::Not {
chat.id.unblock(context).await?;
}
chat.id
};
match step.as_str() {
"vg-member-added"
| "vc-contact-confirm"
@@ -811,7 +788,8 @@ pub(crate) async fn observe_securejoin_on_other_device(
) {
could_not_establish_secure_connection(
context,
contact_chat_id,
contact_id,
info_chat_id(context, contact_id).await?,
"Message not encrypted correctly.",
)
.await?;
@@ -823,7 +801,8 @@ pub(crate) async fn observe_securejoin_on_other_device(
None => {
could_not_establish_secure_connection(
context,
contact_chat_id,
contact_id,
info_chat_id(context, contact_id).await?,
"Fingerprint not provided, please update Delta Chat on all your devices.",
)
.await?;
@@ -833,7 +812,8 @@ pub(crate) async fn observe_securejoin_on_other_device(
if mark_peer_as_verified(context, &fingerprint).await.is_err() {
could_not_establish_secure_connection(
context,
contact_chat_id,
contact_id,
info_chat_id(context, contact_id).await?,
format!("Fingerprint mismatch on observing {}.", step).as_ref(),
)
.await?;
@@ -851,30 +831,22 @@ pub(crate) async fn observe_securejoin_on_other_device(
async fn secure_connection_established(
context: &Context,
contact_chat_id: ChatId,
contact_id: u32,
chat_id: ChatId,
) -> Result<(), Error> {
let contact_id = chat_id_2_contact_id(context, contact_chat_id).await?;
let contact = Contact::get_by_id(context, contact_id).await;
let addr = if let Ok(ref contact) = contact {
contact.get_addr()
} else {
"?"
};
let msg = stock_str::contact_verified(context, addr).await;
chat::add_info_msg(context, contact_chat_id, msg, time()).await?;
context.emit_event(EventType::ChatModified(contact_chat_id));
info!(context, "StockMessage::ContactVerified posted to 1:1 chat");
let contact = Contact::get_by_id(context, contact_id).await?;
let msg = stock_str::contact_verified(context, contact.get_name_n_addr()).await;
chat::add_info_msg(context, chat_id, msg, time()).await?;
context.emit_event(EventType::ChatModified(chat_id));
Ok(())
}
async fn could_not_establish_secure_connection(
context: &Context,
contact_chat_id: ChatId,
contact_id: u32,
chat_id: ChatId,
details: &str,
) -> Result<(), Error> {
let contact_id = chat_id_2_contact_id(context, contact_chat_id).await?;
let contact = Contact::get_by_id(context, contact_id).await;
let msg = stock_str::contact_not_verified(
context,
@@ -885,13 +857,11 @@ async fn could_not_establish_secure_connection(
},
)
.await;
chat::add_info_msg(context, contact_chat_id, &msg, time()).await?;
chat::add_info_msg(context, chat_id, &msg, time()).await?;
error!(
context,
"StockMessage::ContactNotVerified posted to 1:1 chat ({})", details
);
Ok(())
}
@@ -955,10 +925,12 @@ mod tests {
use crate::chat;
use crate::chat::ProtectionStatus;
use crate::chatlist::Chatlist;
use crate::constants::Chattype;
use crate::events::Event;
use crate::peerstate::Peerstate;
use crate::test_utils::TestContext;
use std::time::Duration;
#[async_std::test]
async fn test_setup_contact() -> Result<()> {
@@ -1359,7 +1331,6 @@ mod tests {
};
let sent = bob.pop_sent_msg().await;
assert!(bob.ctx.has_ongoing().await);
assert_eq!(sent.recipient(), "alice@example.com".parse().unwrap());
let msg = alice.parse_msg(&sent).await;
assert!(!msg.was_encrypted());
@@ -1477,6 +1448,12 @@ mod tests {
let bob_chat = Chat::load_from_db(&bob.ctx, bob_chatid).await?;
assert!(bob_chat.is_protected());
assert!(!bob.ctx.has_ongoing().await);
// On this "happy path", Alice and Bob get only a group-chat where all information are added to.
// The one-to-one chats are used internally for the hidden handshake messages,
// however, should not be visible in the UIs.
assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 1);
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 1);
Ok(())
}
}