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

@@ -175,8 +175,8 @@ impl ChatId {
}
/// Same as `create_for_contact()` with an additional `create_blocked` parameter
/// that is used in case the chat does not exist.
/// If the chat exists already, `create_blocked` is ignored.
/// that is used in case the chat does not exist or to unblock existing chats.
/// `create_blocked` won't block already unblocked chats again.
pub(crate) async fn create_for_contact_with_blocked(
context: &Context,
contact_id: u32,
@@ -184,7 +184,7 @@ impl ChatId {
) -> Result<Self> {
let chat_id = match ChatIdBlocked::lookup_by_contact(context, contact_id).await? {
Some(chat) => {
if chat.blocked != Blocked::Not {
if create_blocked == Blocked::Not && chat.blocked != Blocked::Not {
chat.id.unblock(context).await?;
}
chat.id
@@ -304,7 +304,7 @@ impl ChatId {
self.delete(context).await?;
}
Chattype::Mailinglist => {
if self.set_blocked(context, Blocked::Manually).await? {
if self.set_blocked(context, Blocked::Yes).await? {
context.emit_event(EventType::ChatModified(self));
}
}
@@ -3891,7 +3891,7 @@ mod tests {
// create contact, then blocked chat
let contact_id = Contact::create(&ctx, "", "claire@foo.de").await.unwrap();
let chat_id = ChatIdBlocked::get_for_contact(&ctx, contact_id, Blocked::Manually)
let chat_id = ChatIdBlocked::get_for_contact(&ctx, contact_id, Blocked::Yes)
.await
.unwrap()
.id;
@@ -3900,7 +3900,7 @@ mod tests {
.unwrap()
.unwrap();
assert_eq!(chat_id, chat2.id);
assert_eq!(chat2.blocked, Blocked::Manually);
assert_eq!(chat2.blocked, Blocked::Yes);
// test nonexistent contact
let found = ChatId::lookup_by_contact(&ctx, 1234).await.unwrap();
@@ -4410,4 +4410,38 @@ mod tests {
Ok(())
}
#[async_std::test]
async fn test_create_for_contact_with_blocked() -> Result<()> {
let t = TestContext::new().await;
let (contact_id, _) =
Contact::add_or_lookup(&t, "", "foo@bar.org", Origin::ManuallyCreated).await?;
// create a blocked chat
let chat_id_orig =
ChatId::create_for_contact_with_blocked(&t, contact_id, Blocked::Yes).await?;
assert!(!chat_id_orig.is_special());
let chat = Chat::load_from_db(&t, chat_id_orig).await?;
assert_eq!(chat.blocked, Blocked::Yes);
// repeating the call, the same chat must still be blocked
let chat_id = ChatId::create_for_contact_with_blocked(&t, contact_id, Blocked::Yes).await?;
assert_eq!(chat_id, chat_id_orig);
let chat = Chat::load_from_db(&t, chat_id).await?;
assert_eq!(chat.blocked, Blocked::Yes);
// already created chats are unblocked if requested
let chat_id = ChatId::create_for_contact_with_blocked(&t, contact_id, Blocked::Not).await?;
assert_eq!(chat_id, chat_id_orig);
let chat = Chat::load_from_db(&t, chat_id).await?;
assert_eq!(chat.blocked, Blocked::Not);
// however, already created chats are not re-blocked
let chat_id = ChatId::create_for_contact_with_blocked(&t, contact_id, Blocked::Yes).await?;
assert_eq!(chat_id, chat_id_orig);
let chat = Chat::load_from_db(&t, chat_id).await?;
assert_eq!(chat.blocked, Blocked::Not);
Ok(())
}
}

View File

@@ -363,7 +363,7 @@ pub async fn dc_get_archived_cnt(context: &Context) -> Result<usize> {
.sql
.count(
"SELECT COUNT(*) FROM chats WHERE blocked!=? AND archived=?;",
paramsv![Blocked::Manually, ChatVisibility::Archived],
paramsv![Blocked::Yes, ChatVisibility::Archived],
)
.await?;
Ok(count)

View File

@@ -24,7 +24,7 @@ pub static DC_VERSION_STR: Lazy<String> = Lazy::new(|| env!("CARGO_PKG_VERSION")
#[repr(i8)]
pub enum Blocked {
Not = 0,
Manually = 1,
Yes = 1,
Request = 2,
}
@@ -385,7 +385,7 @@ mod tests {
// values may be written to disk and must not change
assert_eq!(Blocked::Not, Blocked::default());
assert_eq!(Blocked::Not, Blocked::from_i32(0).unwrap());
assert_eq!(Blocked::Manually, Blocked::from_i32(1).unwrap());
assert_eq!(Blocked::Yes, Blocked::from_i32(1).unwrap());
assert_eq!(Blocked::Request, Blocked::from_i32(2).unwrap());
}

View File

@@ -711,7 +711,7 @@ impl Contact {
.sql
.query_map(
"SELECT name, grpid FROM chats WHERE type=? AND blocked=?;",
paramsv![Chattype::Mailinglist, Blocked::Manually],
paramsv![Chattype::Mailinglist, Blocked::Yes],
|row| Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?)),
|rows| {
rows.collect::<std::result::Result<Vec<_>, _>>()

View File

@@ -633,7 +633,9 @@ async fn add_parts(
if chat_id.is_none() {
// try to create a normal chat
let create_blocked = if from_id == to_id {
let create_blocked = if *hidden {
Blocked::Yes
} else if from_id == DC_CONTACT_ID_SELF {
Blocked::Not
} else {
Blocked::Request
@@ -798,7 +800,7 @@ async fn add_parts(
if chat_id.is_none() && allow_creation {
let create_blocked = if !Contact::is_blocked_load(context, to_id).await? {
if self_sent && *hidden {
Blocked::Manually
Blocked::Yes
} else {
Blocked::Not
}
@@ -1543,6 +1545,17 @@ async fn create_or_lookup_group(
}
set_better_msg(mime_parser, &better_msg);
let create_protected = if mime_parser.get_header(HeaderDef::ChatVerified).is_some() {
if let Err(err) = check_verified_properties(context, mime_parser, from_id, to_ids).await {
warn!(context, "verification problem: {}", err);
let s = format!("{}. See 'Info' for more details", err);
mime_parser.repl_msg_by_error(&s);
}
ProtectionStatus::Protected
} else {
ProtectionStatus::Unprotected
};
// check if the group does not exist but should be created
let group_explicitly_left = chat::is_group_explicitly_left(context, &grpid)
.await
@@ -1563,18 +1576,6 @@ async fn create_or_lookup_group(
|| X_MrAddToGrp.is_some() && addr_cmp(&self_addr, X_MrAddToGrp.as_ref().unwrap()))
{
// group does not exist but should be created
let create_protected = if mime_parser.get_header(HeaderDef::ChatVerified).is_some() {
if let Err(err) = check_verified_properties(context, mime_parser, from_id, to_ids).await
{
warn!(context, "verification problem: {}", err);
let s = format!("{}. See 'Info' for more details", err);
mime_parser.repl_msg_by_error(&s);
}
ProtectionStatus::Protected
} else {
ProtectionStatus::Unprotected
};
if !allow_creation {
info!(context, "creating group forbidden by caller");
return Ok(None);
@@ -1615,6 +1616,16 @@ async fn create_or_lookup_group(
// .add_protection_msg(context, ProtectionStatus::Protected, false, 0)
// .await?;
//}
} else if let Some(chat_id) = chat_id {
if create_protected == ProtectionStatus::Protected {
let chat = Chat::load_from_db(context, chat_id).await?;
if !chat.is_protected() {
chat_id
.inner_set_protection(context, ProtectionStatus::Protected)
.await?;
recreate_member_list = true;
}
}
}
// again, check chat_id

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(())
}
}

View File

@@ -8,11 +8,11 @@
//! protocol. Afterwards it must be stored in a mutex and the [`BobStateHandle`] should be
//! used to work with the state.
use anyhow::{Error, Result};
use anyhow::{bail, Error, Result};
use async_std::sync::MutexGuard;
use crate::chat::{self, ChatId};
use crate::constants::Viewtype;
use crate::constants::{Blocked, Viewtype};
use crate::contact::{Contact, Origin};
use crate::context::Context;
use crate::events::EventType;
@@ -67,9 +67,18 @@ impl<'a> BobStateHandle<'a> {
})
}
/// Returns the [`ChatId`] of the 1:1 chat with the inviter (Alice).
pub fn chat_id(&self) -> ChatId {
self.bobstate.chat_id
/// Returns the [`ChatId`] of the group chat to join or the 1:1 chat with Alice.
pub async fn chat_id(&self, context: &Context) -> Result<ChatId> {
match self.bobstate.invite {
QrInvite::Group { ref grpid, .. } => {
if let Some((chat_id, _, _)) = chat::get_chat_id_by_grpid(context, &grpid).await? {
Ok(chat_id)
} else {
bail!("chat not found")
}
}
QrInvite::Contact { .. } => Ok(self.bobstate.chat_id),
}
}
/// Returns a reference to the [`QrInvite`] of the joiner process.
@@ -185,10 +194,11 @@ impl BobState {
context: &Context,
invite: QrInvite,
) -> Result<(Self, BobHandshakeStage), JoinError> {
let chat_id = ChatId::create_for_contact(context, invite.contact_id())
.await
.map_err(JoinError::UnknownContact)?;
if fingerprint_equals_sender(context, invite.fingerprint(), chat_id).await? {
let chat_id =
ChatId::create_for_contact_with_blocked(context, invite.contact_id(), Blocked::Yes)
.await
.map_err(JoinError::UnknownContact)?;
if fingerprint_equals_sender(context, invite.fingerprint(), invite.contact_id()).await? {
// The scanned fingerprint matches Alice's key, we can proceed to step 4b.
info!(context, "Taking securejoin protocol shortcut");
let state = Self {
@@ -297,7 +307,9 @@ impl BobState {
self.next = SecureJoinStep::Terminated;
return Ok(Some(BobHandshakeStage::Terminated(reason)));
}
if !fingerprint_equals_sender(context, self.invite.fingerprint(), self.chat_id).await? {
if !fingerprint_equals_sender(context, self.invite.fingerprint(), self.invite.contact_id())
.await?
{
self.next = SecureJoinStep::Terminated;
return Ok(Some(BobHandshakeStage::Terminated("Fingerprint mismatch")));
}

View File

@@ -326,6 +326,13 @@ pub enum StockMessage {
#[strum(props(fallback = "%1$s of %2$s used"))]
PartOfTotallUsed = 116,
#[strum(props(fallback = "%1$s invited you to join this group.\n\n\
Waiting for the device of %2$s to reply…"))]
SecureJoinStarted = 117,
#[strum(props(fallback = "%1$s replied, waiting for being added to the group…"))]
SecureJoinReplies = 118,
}
impl StockMessage {
@@ -591,6 +598,32 @@ pub(crate) async fn e2e_preferred(context: &Context) -> String {
translated(context, StockMessage::E2ePreferred).await
}
/// Stock string: `%1$s invited you to join this group. Waiting for the device of %2$s to reply…`.
pub(crate) async fn secure_join_started(context: &Context, inviter_contact_id: u32) -> String {
if let Ok(contact) = Contact::get_by_id(context, inviter_contact_id).await {
translated(context, StockMessage::SecureJoinStarted)
.await
.replace1(contact.get_name_n_addr())
.replace2(contact.get_display_name())
} else {
format!(
"secure_join_started: unknown contact {}",
inviter_contact_id
)
}
}
/// Stock string: `%1$s replied, waiting for being added to the group…`.
pub(crate) async fn secure_join_replies(context: &Context, contact_id: u32) -> String {
if let Ok(contact) = Contact::get_by_id(context, contact_id).await {
translated(context, StockMessage::SecureJoinReplies)
.await
.replace1(contact.get_display_name())
} else {
format!("secure_join_replies: unknown contact {}", contact_id)
}
}
/// Stock string: `%1$s verified.`.
pub(crate) async fn contact_verified(context: &Context, contact_addr: impl AsRef<str>) -> String {
translated(context, StockMessage::ContactVerified)

View File

@@ -126,12 +126,9 @@ impl Context {
/// Sends out a self-sent message with items to be synchronized, if any.
pub async fn send_sync_msg(&self) -> Result<Option<MsgId>> {
if let Some((json, ids)) = self.build_sync_json().await? {
let chat_id = ChatId::create_for_contact_with_blocked(
self,
DC_CONTACT_ID_SELF,
Blocked::Manually,
)
.await?;
let chat_id =
ChatId::create_for_contact_with_blocked(self, DC_CONTACT_ID_SELF, Blocked::Yes)
.await?;
let mut msg = Message {
chat_id,
viewtype: Viewtype::Text,