mirror of
https://github.com/chatmail/core.git
synced 2026-04-19 22:46:29 +03:00
feat: Transfer the broadcast secret in an encrypted message rather than directly in the QR code
This commit is contained in:
@@ -71,7 +71,7 @@ fn criterion_benchmark(c: &mut Criterion) {
|
||||
});
|
||||
|
||||
b.iter(|| {
|
||||
let mut msg =
|
||||
let (mut msg, _) =
|
||||
decrypt(encrypted.clone().into_bytes(), &[], black_box(&secrets)).unwrap();
|
||||
let decrypted = msg.as_data_vec().unwrap();
|
||||
|
||||
@@ -101,7 +101,7 @@ fn criterion_benchmark(c: &mut Criterion) {
|
||||
});
|
||||
|
||||
b.iter(|| {
|
||||
let mut msg = decrypt(
|
||||
let (mut msg, _) = decrypt(
|
||||
encrypted.clone().into_bytes(),
|
||||
&[key_pair.secret.clone()],
|
||||
black_box(&secrets),
|
||||
|
||||
15
src/chat.rs
15
src/chat.rs
@@ -2941,10 +2941,13 @@ async fn prepare_send_msg(
|
||||
SystemMessage::MemberRemovedFromGroup | SystemMessage::SecurejoinMessage
|
||||
)
|
||||
}
|
||||
CantSendReason::MissingKey => msg
|
||||
.param
|
||||
.get_bool(Param::ForcePlaintext)
|
||||
.unwrap_or_default(),
|
||||
CantSendReason::MissingKey => {
|
||||
msg.param
|
||||
.get_bool(Param::ForcePlaintext)
|
||||
.unwrap_or_default()
|
||||
// V2 securejoin messages are symmetrically encrypted, no need for the public key:
|
||||
|| msg.securejoin_step() == Some("vb-request-v2")
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
if let Some(reason) = chat.why_cant_send_ex(context, &skip_fn).await? {
|
||||
@@ -3878,11 +3881,13 @@ pub(crate) async fn save_broadcast_shared_secret(
|
||||
chat_id: ChatId,
|
||||
secret: &str,
|
||||
) -> Result<()> {
|
||||
info!(context, "Saving broadcast secret for chat {chat_id}");
|
||||
info!(context, "dbg the new secret for chat {chat_id} is {secret}");
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"INSERT INTO broadcasts_shared_secrets (chat_id, secret) VALUES (?, ?)
|
||||
ON CONFLICT(chat_id) DO UPDATE SET secret=excluded.chat_id",
|
||||
ON CONFLICT(chat_id) DO UPDATE SET secret=excluded.secret",
|
||||
(chat_id, secret),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -7,7 +7,7 @@ use crate::imex::{ImexMode, has_backup, imex};
|
||||
use crate::message::{MessengerMessage, delete_msgs};
|
||||
use crate::mimeparser::{self, MimeMessage};
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::securejoin::get_securejoin_qr;
|
||||
use crate::securejoin::{get_securejoin_qr, join_securejoin};
|
||||
use crate::test_utils::{
|
||||
AVATAR_64x64_BYTES, AVATAR_64x64_DEDUPLICATED, E2EE_INFO_MSGS, TestContext, TestContextManager,
|
||||
TimeShiftFalsePositiveNote, sync,
|
||||
@@ -3094,8 +3094,12 @@ async fn test_leave_broadcast_multidevice() -> Result<()> {
|
||||
tcm.section("Alice creates broadcast channel with Bob.");
|
||||
let alice_chat_id = create_broadcast(alice, "foo".to_string()).await?;
|
||||
let qr = get_securejoin_qr(alice, Some(alice_chat_id)).await.unwrap();
|
||||
tcm.exec_securejoin_qr(bob0, alice, &qr).await;
|
||||
sync(bob0, bob1).await;
|
||||
join_securejoin(bob0, &qr).await.unwrap();
|
||||
let request = bob0.pop_sent_msg().await;
|
||||
alice.recv_msg(&request).await;
|
||||
let answer = alice.pop_sent_msg().await;
|
||||
bob0.recv_msg(&answer).await;
|
||||
bob1.recv_msg(&answer).await;
|
||||
|
||||
tcm.section("Alice sends first message to broadcast.");
|
||||
let sent_msg = alice.send_text(alice_chat_id, "Hello!").await;
|
||||
|
||||
@@ -10,18 +10,22 @@ use crate::pgp;
|
||||
|
||||
/// Tries to decrypt a message, but only if it is structured as an Autocrypt message.
|
||||
///
|
||||
/// If successful and the message is encrypted, returns decrypted body.
|
||||
/// If successful and the message is encrypted, returns a tuple of:
|
||||
///
|
||||
/// - The decrypted and decompressed message
|
||||
/// - If the message was symmetrically encrypted:
|
||||
/// The index in `shared_secrets` of the secret used to decrypt the message.
|
||||
pub fn try_decrypt<'a>(
|
||||
mail: &'a ParsedMail<'a>,
|
||||
private_keyring: &'a [SignedSecretKey],
|
||||
symmetric_secrets: &[String],
|
||||
) -> Result<Option<::pgp::composed::Message<'static>>> {
|
||||
shared_secrets: &[String],
|
||||
) -> Result<Option<(::pgp::composed::Message<'static>, Option<usize>)>> {
|
||||
let Some(encrypted_data_part) = get_encrypted_mime(mail) else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let data = encrypted_data_part.get_body_raw()?;
|
||||
let msg = pgp::decrypt(data, private_keyring, symmetric_secrets)?;
|
||||
let msg = pgp::decrypt(data, private_keyring, shared_secrets)?;
|
||||
|
||||
Ok(Some(msg))
|
||||
}
|
||||
|
||||
@@ -1385,6 +1385,18 @@ impl Message {
|
||||
pub fn error(&self) -> Option<String> {
|
||||
self.error.clone()
|
||||
}
|
||||
|
||||
// TODO this function could be used a lot more
|
||||
/// If this is a secure-join message,
|
||||
/// returns the current step,
|
||||
/// which is put into the `Secure-Join` header.
|
||||
pub(crate) fn securejoin_step(&self) -> Option<&str> {
|
||||
if self.param.get_cmd() == SystemMessage::SecurejoinMessage {
|
||||
self.param.get(Param::Arg)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// State of the message.
|
||||
|
||||
@@ -329,7 +329,7 @@ impl MimeFactory {
|
||||
|
||||
if let Some(public_key) = public_key_opt {
|
||||
keys.push((addr.clone(), public_key))
|
||||
} else if id != ContactId::SELF && !chat.is_any_broadcast() {
|
||||
} else if id != ContactId::SELF && !should_encrypt_symmetrically(&msg, &chat) {
|
||||
missing_key_addresses.insert(addr.clone());
|
||||
if is_encrypted {
|
||||
warn!(context, "Missing key for {addr}");
|
||||
@@ -350,7 +350,7 @@ impl MimeFactory {
|
||||
|
||||
if let Some(public_key) = public_key_opt {
|
||||
keys.push((addr.clone(), public_key))
|
||||
} else if id != ContactId::SELF && !chat.is_any_broadcast() {
|
||||
} else if id != ContactId::SELF && !should_encrypt_symmetrically(&msg, &chat) {
|
||||
missing_key_addresses.insert(addr.clone());
|
||||
if is_encrypted {
|
||||
warn!(context, "Missing key for {addr}");
|
||||
@@ -430,7 +430,7 @@ impl MimeFactory {
|
||||
|
||||
encryption_keys = if !is_encrypted {
|
||||
None
|
||||
} else if chat.is_out_broadcast() {
|
||||
} else if should_encrypt_symmetrically(&msg, &chat) {
|
||||
// Encrypt, but only symmetrically, not with the public keys.
|
||||
Some(Vec::new())
|
||||
} else {
|
||||
@@ -579,7 +579,7 @@ impl MimeFactory {
|
||||
// messages are auto-sent unlike usual unencrypted messages.
|
||||
step == "vg-request-with-auth"
|
||||
|| step == "vc-request-with-auth"
|
||||
|| step == "vb-request-with-auth"
|
||||
|| step == "vb-request-v2"
|
||||
|| step == "vg-member-added"
|
||||
|| step == "vb-member-added"
|
||||
|| step == "vc-contact-confirm"
|
||||
@@ -825,7 +825,7 @@ impl MimeFactory {
|
||||
} else if let Loaded::Message { msg, .. } = &self.loaded {
|
||||
if msg.param.get_cmd() == SystemMessage::SecurejoinMessage {
|
||||
let step = msg.param.get(Param::Arg).unwrap_or_default();
|
||||
if step != "vg-request" && step != "vc-request" {
|
||||
if step != "vg-request" && step != "vc-request" && step != "vb-request-v2" {
|
||||
headers.push((
|
||||
"Auto-Submitted",
|
||||
mail_builder::headers::raw::Raw::new("auto-replied".to_string()).into(),
|
||||
@@ -1181,7 +1181,13 @@ impl MimeFactory {
|
||||
};
|
||||
|
||||
let symmetric_key: Option<String> = match &self.loaded {
|
||||
Loaded::Message { chat, .. } if chat.is_any_broadcast() => {
|
||||
Loaded::Message { msg, .. } if should_encrypt_with_auth_token(msg) => {
|
||||
// TODO rather than setting Arg2, bob.rs could set a param `Param::SharedSecretForEncryption` or similar
|
||||
msg.param.get(Param::Arg2).map(|s| s.to_string())
|
||||
}
|
||||
Loaded::Message { chat, msg }
|
||||
if should_encrypt_with_broadcast_secret(msg, chat) =>
|
||||
{
|
||||
// If there is no symmetric key yet
|
||||
// (because this is an old broadcast channel,
|
||||
// created before we had symmetric encryption),
|
||||
@@ -1196,6 +1202,7 @@ impl MimeFactory {
|
||||
|
||||
let encrypted = if let Some(symmetric_key) = symmetric_key {
|
||||
info!(context, "Symmetrically encrypting for broadcast channel.");
|
||||
info!(context, "secret: {symmetric_key}"); // TODO
|
||||
encrypt_helper
|
||||
.encrypt_for_broadcast(context, &symmetric_key, message, compress)
|
||||
.await?
|
||||
@@ -1547,7 +1554,7 @@ impl MimeFactory {
|
||||
headers.push((
|
||||
if step == "vg-request-with-auth"
|
||||
|| step == "vc-request-with-auth"
|
||||
|| step == "vb-request-with-auth"
|
||||
|| step == "vb-request-v2"
|
||||
{
|
||||
"Secure-Join-Auth"
|
||||
} else {
|
||||
@@ -1898,6 +1905,22 @@ fn hidden_recipients() -> Address<'static> {
|
||||
Address::new_group(Some("hidden-recipients".to_string()), Vec::new())
|
||||
}
|
||||
|
||||
fn should_encrypt_with_auth_token(msg: &Message) -> bool {
|
||||
msg.param.get_cmd() == SystemMessage::SecurejoinMessage
|
||||
&& msg.param.get(Param::Arg).unwrap_or_default() == "vb-request-v2"
|
||||
}
|
||||
|
||||
fn should_encrypt_with_broadcast_secret(msg: &Message, chat: &Chat) -> bool {
|
||||
chat.is_any_broadcast()
|
||||
&& msg.param.get_cmd() != SystemMessage::SecurejoinMessage
|
||||
// The member-added message in a broadcast must be asymmetrirally encrypted:
|
||||
&& msg.param.get_cmd() != SystemMessage::MemberAddedToGroup
|
||||
}
|
||||
|
||||
fn should_encrypt_symmetrically(msg: &Message, chat: &Chat) -> bool {
|
||||
should_encrypt_with_auth_token(msg) || should_encrypt_with_broadcast_secret(msg, chat)
|
||||
}
|
||||
|
||||
async fn build_body_file(context: &Context, msg: &Message) -> Result<MimePart<'static>> {
|
||||
let file_name = msg.get_filename().context("msg has no file")?;
|
||||
let blob = msg
|
||||
|
||||
@@ -18,7 +18,6 @@ use crate::authres::handle_authres;
|
||||
use crate::blob::BlobObject;
|
||||
use crate::chat::ChatId;
|
||||
use crate::config::Config;
|
||||
use crate::constants;
|
||||
use crate::contact::ContactId;
|
||||
use crate::context::Context;
|
||||
use crate::decrypt::{try_decrypt, validate_detached_signature};
|
||||
@@ -35,6 +34,7 @@ use crate::tools::{
|
||||
get_filemeta, parse_receive_headers, smeared_time, time, truncate_msg_text, validate_id,
|
||||
};
|
||||
use crate::{chatlist_events, location, stock_str, tools};
|
||||
use crate::{constants, token};
|
||||
|
||||
/// A parsed MIME message.
|
||||
///
|
||||
@@ -136,6 +136,10 @@ pub(crate) struct MimeMessage {
|
||||
/// Sender timestamp in secs since epoch. Allowed to be in the future due to unsynchronized
|
||||
/// clocks, but not too much.
|
||||
pub(crate) timestamp_sent: i64,
|
||||
|
||||
/// How the message was encrypted (and now successfully decrypted):
|
||||
/// The asymmetric key, an AUTH token, or a broadcast's shared secret.
|
||||
pub(crate) was_encrypted_with: EncryptedWith,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
@@ -234,6 +238,25 @@ pub enum SystemMessage {
|
||||
CallEnded = 67,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum EncryptedWith {
|
||||
AsymmetricKey,
|
||||
BroadcastSecret(String),
|
||||
AuthToken(String),
|
||||
None,
|
||||
}
|
||||
|
||||
impl EncryptedWith {
|
||||
pub(crate) fn auth_token(&self) -> Option<&str> {
|
||||
match self {
|
||||
EncryptedWith::AsymmetricKey => None,
|
||||
EncryptedWith::BroadcastSecret(_) => None,
|
||||
EncryptedWith::AuthToken(token) => Some(token),
|
||||
EncryptedWith::None => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const MIME_AC_SETUP_FILE: &str = "application/autocrypt-setup";
|
||||
|
||||
impl MimeMessage {
|
||||
@@ -354,7 +377,7 @@ impl MimeMessage {
|
||||
|
||||
let mail_raw; // Memory location for a possible decrypted message.
|
||||
let decrypted_msg; // Decrypted signed OpenPGP message.
|
||||
let symmetric_secrets: Vec<String> = context
|
||||
let mut secrets: Vec<String> = context
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT secret FROM broadcasts_shared_secrets",
|
||||
@@ -366,11 +389,13 @@ impl MimeMessage {
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
let num_broadcast_secrets = secrets.len();
|
||||
secrets.extend(token::lookup_all(context, token::Namespace::Auth).await?);
|
||||
|
||||
let (mail, is_encrypted) = match tokio::task::block_in_place(|| {
|
||||
try_decrypt(&mail, &private_keyring, &symmetric_secrets)
|
||||
let (mail, is_encrypted, decrypted_with) = match tokio::task::block_in_place(|| {
|
||||
try_decrypt(&mail, &private_keyring, &secrets)
|
||||
}) {
|
||||
Ok(Some(mut msg)) => {
|
||||
Ok(Some((mut msg, index_of_secret))) => {
|
||||
mail_raw = msg.as_data_vec().unwrap_or_default();
|
||||
|
||||
let decrypted_mail = mailparse::parse_mail(&mail_raw)?;
|
||||
@@ -397,18 +422,29 @@ impl MimeMessage {
|
||||
aheader_value = Some(protected_aheader_value);
|
||||
}
|
||||
|
||||
(Ok(decrypted_mail), true)
|
||||
let decrypted_with = if let Some(index_of_secret) = index_of_secret {
|
||||
let used_secret = secrets.into_iter().nth(index_of_secret).unwrap_or_default();
|
||||
if index_of_secret < num_broadcast_secrets {
|
||||
EncryptedWith::BroadcastSecret(used_secret)
|
||||
} else {
|
||||
EncryptedWith::AuthToken(used_secret)
|
||||
}
|
||||
} else {
|
||||
EncryptedWith::AsymmetricKey
|
||||
};
|
||||
|
||||
(Ok(decrypted_mail), true, decrypted_with)
|
||||
}
|
||||
Ok(None) => {
|
||||
mail_raw = Vec::new();
|
||||
decrypted_msg = None;
|
||||
(Ok(mail), false)
|
||||
(Ok(mail), false, EncryptedWith::None)
|
||||
}
|
||||
Err(err) => {
|
||||
mail_raw = Vec::new();
|
||||
decrypted_msg = None;
|
||||
warn!(context, "decryption failed: {:#}", err);
|
||||
(Err(err), false)
|
||||
(Err(err), false, EncryptedWith::None)
|
||||
}
|
||||
};
|
||||
|
||||
@@ -620,6 +656,7 @@ impl MimeMessage {
|
||||
is_bot: None,
|
||||
timestamp_rcvd,
|
||||
timestamp_sent,
|
||||
was_encrypted_with: decrypted_with,
|
||||
};
|
||||
|
||||
match partial {
|
||||
|
||||
@@ -103,6 +103,9 @@ pub enum Param {
|
||||
/// removed from the group.
|
||||
///
|
||||
/// For "MemberAddedToGroup" this is the email address added to the group.
|
||||
///
|
||||
/// For securejoin messages, this is the step,
|
||||
/// which is put into the `Secure-Join` header.
|
||||
Arg = b'E',
|
||||
|
||||
/// For Messages
|
||||
@@ -111,6 +114,9 @@ pub enum Param {
|
||||
///
|
||||
/// For `BobHandshakeMsg::RequestWithAuth`, this is the `Secure-Join-Auth` header.
|
||||
///
|
||||
/// For version two of the securejoin protocol (`vb-request-v2`),
|
||||
/// this is the Auth token used to encrypt the message.
|
||||
///
|
||||
/// For [`SystemMessage::MultiDeviceSync`], this contains the ids that are synced.
|
||||
///
|
||||
/// For [`SystemMessage::MemberAddedToGroup`],
|
||||
|
||||
32
src/pgp.rs
32
src/pgp.rs
@@ -8,7 +8,7 @@ use chrono::SubsecRound;
|
||||
use deltachat_contact_tools::EmailAddress;
|
||||
use pgp::armor::BlockType;
|
||||
use pgp::composed::{
|
||||
ArmorOptions, Deserializable, KeyType as PgpKeyType, Message, MessageBuilder,
|
||||
ArmorOptions, Deserializable, InnerRingResult, KeyType as PgpKeyType, Message, MessageBuilder,
|
||||
SecretKeyParamsBuilder, SignedPublicKey, SignedPublicSubKey, SignedSecretKey,
|
||||
StandaloneSignature, SubkeyParamsBuilder, TheRing,
|
||||
};
|
||||
@@ -239,13 +239,19 @@ pub fn pk_calc_signature(
|
||||
|
||||
/// Decrypts the message with keys from the private key keyring.
|
||||
///
|
||||
/// Receiver private keys are provided in
|
||||
/// `private_keys_for_decryption`.
|
||||
/// Receiver private keys are passed in `private_keys_for_decryption`,
|
||||
/// shared secrets used for symmetric encryption
|
||||
/// are passed in `shared_secrets`.
|
||||
///
|
||||
/// Returns a tuple of:
|
||||
/// - The decrypted and decompressed message
|
||||
/// - If the message was symmetrically encrypted:
|
||||
/// The index in `shared_secrets` of the secret used to decrypt the message.
|
||||
pub fn decrypt(
|
||||
ctext: Vec<u8>,
|
||||
private_keys_for_decryption: &[SignedSecretKey],
|
||||
symmetric_secrets: &[String],
|
||||
) -> Result<pgp::composed::Message<'static>> {
|
||||
shared_secrets: &[String],
|
||||
) -> Result<(pgp::composed::Message<'static>, Option<usize>)> {
|
||||
let cursor = Cursor::new(ctext);
|
||||
let (msg, _headers) = Message::from_armor(cursor)?;
|
||||
|
||||
@@ -253,7 +259,7 @@ pub fn decrypt(
|
||||
let empty_pw = Password::empty();
|
||||
|
||||
// TODO it may degrade performance that we always try out all passwords here
|
||||
let message_password: Vec<Password> = symmetric_secrets
|
||||
let message_password: Vec<Password> = shared_secrets
|
||||
.iter()
|
||||
.map(|p| Password::from(p.as_str()))
|
||||
.collect();
|
||||
@@ -266,12 +272,17 @@ pub fn decrypt(
|
||||
session_keys: vec![],
|
||||
allow_legacy: false,
|
||||
};
|
||||
let (msg, _ring_result) = msg.decrypt_the_ring(ring, true)?;
|
||||
let (msg, ring_result) = msg.decrypt_the_ring(ring, true)?;
|
||||
|
||||
// remove one layer of compression
|
||||
let msg = msg.decompress()?;
|
||||
|
||||
Ok(msg)
|
||||
let decrypted_with_secret = ring_result
|
||||
.message_password
|
||||
.iter()
|
||||
.position(|&p| p == InnerRingResult::Ok);
|
||||
|
||||
Ok((msg, decrypted_with_secret))
|
||||
}
|
||||
|
||||
/// Returns fingerprints
|
||||
@@ -407,7 +418,7 @@ mod tests {
|
||||
HashSet<Fingerprint>,
|
||||
Vec<u8>,
|
||||
)> {
|
||||
let mut msg = decrypt(ctext.to_vec(), private_keys_for_decryption, &[])?;
|
||||
let (mut msg, _) = decrypt(ctext.to_vec(), private_keys_for_decryption, &[])?;
|
||||
let content = msg.as_data_vec()?;
|
||||
let ret_signature_fingerprints =
|
||||
valid_signature_fingerprints(&msg, public_keys_for_validation);
|
||||
@@ -611,13 +622,14 @@ mod tests {
|
||||
.await?;
|
||||
|
||||
let bob_private_keyring = crate::key::load_self_secret_keyring(bob).await?;
|
||||
let mut decrypted = decrypt(
|
||||
let (mut decrypted, index_of_secret) = decrypt(
|
||||
ctext.into(),
|
||||
&bob_private_keyring,
|
||||
&[shared_secret.to_string()],
|
||||
)?;
|
||||
|
||||
assert_eq!(decrypted.as_data_vec()?, plain);
|
||||
assert_eq!(index_of_secret, Some(0));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -296,7 +296,7 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
// -> or just ignore the problem for now - we will need to solve it for all messages anyways: https://github.com/chatmail/core/issues/7057
|
||||
if !matches!(
|
||||
step,
|
||||
"vg-request" | "vc-request" | "vb-request-with-auth" | "vb-member-added"
|
||||
"vg-request" | "vc-request" | "vb-request-v2" | "vb-member-added"
|
||||
) {
|
||||
let mut self_found = false;
|
||||
let self_fingerprint = load_self_public_key(context).await?.dc_fingerprint();
|
||||
@@ -367,7 +367,7 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
========================================================*/
|
||||
bob::handle_auth_required(context, mime_message).await
|
||||
}
|
||||
"vg-request-with-auth" | "vc-request-with-auth" | "vb-request-with-auth" => {
|
||||
"vg-request-with-auth" | "vc-request-with-auth" | "vb-request-v2" => {
|
||||
/*==========================================================
|
||||
==== Alice - the inviter side ====
|
||||
==== Steps 5+6 in "Setup verified contact" protocol ====
|
||||
@@ -390,8 +390,12 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
);
|
||||
return Ok(HandshakeMessage::Ignore);
|
||||
}
|
||||
// verify that the `Secure-Join-Auth:`-header matches the secret written to the QR code
|
||||
let Some(auth) = mime_message.get_header(HeaderDef::SecureJoinAuth) else {
|
||||
// verify that the `Secure-Join-Auth:`-header matches the secret written to the QR code,
|
||||
// or that the message was encrypted with the secret written to the QR code.
|
||||
let auth = mime_message
|
||||
.get_header(HeaderDef::SecureJoinAuth)
|
||||
.or_else(|| mime_message.was_encrypted_with.auth_token());
|
||||
let Some(auth) = auth else {
|
||||
warn!(
|
||||
context,
|
||||
"Ignoring {step} message because of missing auth code."
|
||||
@@ -447,9 +451,16 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
.await?;
|
||||
inviter_progress(context, contact_id, 800);
|
||||
inviter_progress(context, contact_id, 1000);
|
||||
// IMAP-delete the message to avoid handling it by another device and adding the
|
||||
// member twice. Another device will know the member's key from Autocrypt-Gossip.
|
||||
Ok(HandshakeMessage::Done)
|
||||
if step.starts_with("vb-") {
|
||||
// For broadcasts, we don't want to delete the message,
|
||||
// because the other device should also internally add the member
|
||||
// and see the key (because it won't see the member via autocrypt-gossip).
|
||||
Ok(HandshakeMessage::Propagate)
|
||||
} else {
|
||||
// IMAP-delete the message to avoid handling it by another device and adding the
|
||||
// member twice. Another device will know the member's key from Autocrypt-Gossip.
|
||||
Ok(HandshakeMessage::Done)
|
||||
}
|
||||
} else {
|
||||
// Setup verified contact.
|
||||
secure_connection_established(
|
||||
@@ -594,7 +605,7 @@ pub(crate) async fn observe_securejoin_on_other_device(
|
||||
inviter_progress(context, contact_id, 1000);
|
||||
}
|
||||
|
||||
// TODO not sure if I should ad vb-request-with-auth here
|
||||
// TODO not sure if I should add vb-request-v2 here
|
||||
// Actually, I'm not even sure why vg-request-with-auth is here - why do we create a 1:1 chat??
|
||||
if step == "vg-request-with-auth" || step == "vc-request-with-auth" {
|
||||
// This actually reflects what happens on the first device (which does the secure
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
//! Bob's side of SecureJoin handling, the joiner-side.
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use anyhow::{Context as _, Result, bail};
|
||||
|
||||
use super::HandshakeMessage;
|
||||
use super::qrinvite::QrInvite;
|
||||
use crate::chat::{
|
||||
self, ChatId, ProtectionStatus, is_contact_in_chat, save_broadcast_shared_secret,
|
||||
};
|
||||
use crate::chat::{self, ChatId, ProtectionStatus, is_contact_in_chat};
|
||||
use crate::constants::{Blocked, Chattype};
|
||||
use crate::contact::Origin;
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use crate::key::self_fingerprint;
|
||||
use crate::log::{LogExt as _, info};
|
||||
use crate::log::info;
|
||||
use crate::message::{Message, Viewtype};
|
||||
use crate::mimeparser::{MimeMessage, SystemMessage};
|
||||
use crate::param::Param;
|
||||
@@ -51,63 +49,48 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul
|
||||
QrInvite::Group { .. } => Blocked::Yes,
|
||||
QrInvite::Broadcast { .. } => Blocked::Yes,
|
||||
};
|
||||
let chat_id = ChatId::create_for_contact_with_blocked(context, invite.contact_id(), hidden)
|
||||
.await
|
||||
.with_context(|| format!("can't create chat for contact {}", invite.contact_id()))?;
|
||||
|
||||
// The 1:1 chat with the inviter
|
||||
let private_chat_id =
|
||||
ChatId::create_for_contact_with_blocked(context, invite.contact_id(), hidden)
|
||||
.await
|
||||
.with_context(|| format!("can't create chat for contact {}", invite.contact_id()))?;
|
||||
|
||||
// The chat id of the 1:1 chat, group or broadcast that is being joined
|
||||
let joining_chat_id = joining_chat_id(context, &invite, private_chat_id).await?;
|
||||
|
||||
ContactId::scaleup_origin(context, &[invite.contact_id()], Origin::SecurejoinJoined).await?;
|
||||
context.emit_event(EventType::ContactsChanged(None));
|
||||
|
||||
if let QrInvite::Broadcast {
|
||||
shared_secret,
|
||||
grpid,
|
||||
broadcast_name,
|
||||
..
|
||||
} = &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?;
|
||||
|
||||
save_broadcast_shared_secret(context, broadcast_chat_id, shared_secret).await?;
|
||||
|
||||
let id = chat::SyncId::Grpid(grpid.to_string());
|
||||
let action = chat::SyncAction::CreateInBroadcast {
|
||||
chat_name: broadcast_name.to_string(),
|
||||
shared_secret: shared_secret.to_string(),
|
||||
};
|
||||
chat::sync(context, id, action).await.log_err(context).ok();
|
||||
|
||||
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(),
|
||||
});
|
||||
if invite.is_v2() {
|
||||
if !verify_sender_by_fingerprint(context, invite.fingerprint(), invite.contact_id()).await?
|
||||
{
|
||||
bail!("V2 protocol failed because of fingerprint mismatch");
|
||||
}
|
||||
info!(context, "Using fast securejoin with symmetric encryption");
|
||||
|
||||
let mut msg = Message {
|
||||
viewtype: Viewtype::Text,
|
||||
text: "Secure-Join: vb-request-v2".to_string(),
|
||||
hidden: true,
|
||||
..Default::default()
|
||||
};
|
||||
msg.param.set_cmd(SystemMessage::SecurejoinMessage);
|
||||
|
||||
msg.param.set(Param::Arg, "vb-request-v2");
|
||||
msg.param.set(Param::Arg2, invite.authcode());
|
||||
msg.param.set_int(Param::GuaranteeE2ee, 1);
|
||||
let bob_fp = self_fingerprint(context).await?;
|
||||
msg.param.set(Param::Arg3, bob_fp);
|
||||
|
||||
chat::send_msg(context, private_chat_id, &mut msg).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.
|
||||
// Start the version 1 protocol and initialise the state.
|
||||
let has_key = context
|
||||
.sql
|
||||
.exists(
|
||||
@@ -122,11 +105,16 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul
|
||||
{
|
||||
// The scanned fingerprint matches Alice's key, we can proceed to step 4b.
|
||||
info!(context, "Taking securejoin protocol shortcut");
|
||||
send_handshake_message(context, &invite, chat_id, BobHandshakeMsg::RequestWithAuth)
|
||||
.await?;
|
||||
send_handshake_message(
|
||||
context,
|
||||
&invite,
|
||||
private_chat_id,
|
||||
BobHandshakeMsg::RequestWithAuth,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Mark 1:1 chat as verified already.
|
||||
chat_id
|
||||
private_chat_id
|
||||
.set_protection(
|
||||
context,
|
||||
ProtectionStatus::Protected,
|
||||
@@ -140,9 +128,10 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul
|
||||
progress: JoinerProgress::RequestWithAuthSent.to_usize(),
|
||||
});
|
||||
} else {
|
||||
send_handshake_message(context, &invite, chat_id, BobHandshakeMsg::Request).await?;
|
||||
send_handshake_message(context, &invite, private_chat_id, BobHandshakeMsg::Request)
|
||||
.await?;
|
||||
|
||||
insert_new_db_entry(context, invite.clone(), chat_id).await?;
|
||||
insert_new_db_entry(context, invite.clone(), private_chat_id).await?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,28 +139,26 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul
|
||||
QrInvite::Group { .. } => {
|
||||
// For a secure-join we need to create the group and add the contact. The group will
|
||||
// only become usable once the protocol is finished.
|
||||
let group_chat_id = joining_chat_id(context, &invite, chat_id).await?;
|
||||
if !is_contact_in_chat(context, group_chat_id, invite.contact_id()).await? {
|
||||
if !is_contact_in_chat(context, joining_chat_id, invite.contact_id()).await? {
|
||||
chat::add_to_chat_contacts_table(
|
||||
context,
|
||||
time(),
|
||||
group_chat_id,
|
||||
joining_chat_id,
|
||||
&[invite.contact_id()],
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
let msg = stock_str::secure_join_started(context, invite.contact_id()).await;
|
||||
chat::add_info_msg(context, group_chat_id, &msg, time()).await?;
|
||||
Ok(group_chat_id)
|
||||
chat::add_info_msg(context, joining_chat_id, &msg, time()).await?;
|
||||
Ok(joining_chat_id)
|
||||
}
|
||||
QrInvite::Broadcast { .. } => {
|
||||
// TODO code duplication with previous block
|
||||
let broadcast_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, joining_chat_id, invite.contact_id()).await? {
|
||||
chat::add_to_chat_contacts_table(
|
||||
context,
|
||||
time(),
|
||||
broadcast_chat_id,
|
||||
joining_chat_id,
|
||||
&[invite.contact_id()],
|
||||
)
|
||||
.await?;
|
||||
@@ -179,8 +166,8 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul
|
||||
|
||||
// TODO this message should be translatable:
|
||||
let msg = "You were invited to join this channel. Waiting for the channel owner's device to reply…";
|
||||
chat::add_info_msg(context, broadcast_chat_id, msg, time()).await?;
|
||||
Ok(broadcast_chat_id)
|
||||
chat::add_info_msg(context, joining_chat_id, msg, time()).await?;
|
||||
Ok(joining_chat_id)
|
||||
}
|
||||
QrInvite::Contact { .. } => {
|
||||
// For setup-contact the BobState already ensured the 1:1 chat exists because it
|
||||
@@ -189,14 +176,14 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul
|
||||
// race with its change, we don't add our message below the protection message.
|
||||
let sort_to_bottom = true;
|
||||
let (received, incoming) = (false, false);
|
||||
let ts_sort = chat_id
|
||||
let ts_sort = private_chat_id
|
||||
.calc_sort_timestamp(context, 0, sort_to_bottom, received, incoming)
|
||||
.await?;
|
||||
if chat_id.is_protected(context).await? == ProtectionStatus::Unprotected {
|
||||
if private_chat_id.is_protected(context).await? == ProtectionStatus::Unprotected {
|
||||
let ts_start = time();
|
||||
chat::add_info_msg_with_cmd(
|
||||
context,
|
||||
chat_id,
|
||||
private_chat_id,
|
||||
&stock_str::securejoin_wait(context).await,
|
||||
SystemMessage::SecurejoinWait,
|
||||
ts_sort,
|
||||
@@ -207,7 +194,7 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
Ok(chat_id)
|
||||
Ok(private_chat_id)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -334,7 +321,7 @@ pub(crate) async fn send_handshake_message(
|
||||
match step {
|
||||
BobHandshakeMsg::Request => {
|
||||
// Sends the Secure-Join-Invitenumber header in mimefactory.rs.
|
||||
msg.param.set(Param::Arg2, invite.invitenumber());
|
||||
msg.param.set_optional(Param::Arg2, invite.invitenumber());
|
||||
msg.force_plaintext();
|
||||
}
|
||||
BobHandshakeMsg::RequestWithAuth => {
|
||||
@@ -368,7 +355,7 @@ pub(crate) async fn send_handshake_message(
|
||||
pub(crate) enum BobHandshakeMsg {
|
||||
/// vc-request or vg-request
|
||||
Request,
|
||||
/// vc-request-with-auth, vg-request-with-auth, or vb-request-with-auth
|
||||
/// vc-request-with-auth, vg-request-with-auth, or vb-request-v2
|
||||
RequestWithAuth,
|
||||
}
|
||||
|
||||
@@ -399,7 +386,9 @@ impl BobHandshakeMsg {
|
||||
Self::RequestWithAuth => match invite {
|
||||
QrInvite::Contact { .. } => "vc-request-with-auth",
|
||||
QrInvite::Group { .. } => "vg-request-with-auth",
|
||||
QrInvite::Broadcast { .. } => "vb-request-with-auth",
|
||||
QrInvite::Broadcast { .. } => {
|
||||
panic!("There is no request-with-auth for broadcasts")
|
||||
} // TODO remove panic
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ pub enum QrInvite {
|
||||
Contact {
|
||||
contact_id: ContactId,
|
||||
fingerprint: Fingerprint,
|
||||
invitenumber: String,
|
||||
invitenumber: Option<String>,
|
||||
authcode: String,
|
||||
},
|
||||
Group {
|
||||
@@ -26,7 +26,7 @@ pub enum QrInvite {
|
||||
fingerprint: Fingerprint,
|
||||
name: String,
|
||||
grpid: String,
|
||||
invitenumber: String,
|
||||
invitenumber: Option<String>,
|
||||
authcode: String,
|
||||
},
|
||||
Broadcast {
|
||||
@@ -62,10 +62,12 @@ impl QrInvite {
|
||||
}
|
||||
|
||||
/// The `INVITENUMBER` of the setup-contact/secure-join protocol.
|
||||
pub fn invitenumber(&self) -> &str {
|
||||
pub fn invitenumber(&self) -> Option<&str> {
|
||||
match self {
|
||||
Self::Contact { invitenumber, .. } | Self::Group { invitenumber, .. } => invitenumber,
|
||||
Self::Broadcast { .. } => panic!("broadcast invite has no invite number"), // TODO panic
|
||||
Self::Contact { invitenumber, .. } | Self::Group { invitenumber, .. } => {
|
||||
invitenumber.as_deref()
|
||||
}
|
||||
Self::Broadcast { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,6 +79,17 @@ impl QrInvite {
|
||||
| Self::Broadcast { authcode, .. } => authcode,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether this QR code uses the faster "version 2" protocol,
|
||||
/// where the first message from Bob to Alice is symmetrically encrypted
|
||||
/// with the AUTH code.
|
||||
/// We may decide in the future to backwards-compatibly mark QR codes as V2,
|
||||
/// but for now, everything without an invite number
|
||||
/// is definitely V2,
|
||||
/// because the invite number is needed for V1.
|
||||
pub(crate) fn is_v2(&self) -> bool {
|
||||
self.invitenumber().is_none()
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Qr> for QrInvite {
|
||||
@@ -92,7 +105,7 @@ impl TryFrom<Qr> for QrInvite {
|
||||
} => Ok(QrInvite::Contact {
|
||||
contact_id,
|
||||
fingerprint,
|
||||
invitenumber,
|
||||
invitenumber: Some(invitenumber),
|
||||
authcode,
|
||||
}),
|
||||
Qr::AskVerifyGroup {
|
||||
@@ -107,7 +120,7 @@ impl TryFrom<Qr> for QrInvite {
|
||||
fingerprint,
|
||||
name: grpname,
|
||||
grpid,
|
||||
invitenumber,
|
||||
invitenumber: Some(invitenumber),
|
||||
authcode,
|
||||
}),
|
||||
Qr::AskJoinBroadcast {
|
||||
|
||||
15
src/token.rs
15
src/token.rs
@@ -61,6 +61,21 @@ pub async fn lookup(
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn lookup_all(context: &Context, namespace: Namespace) -> Result<Vec<String>> {
|
||||
context
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT token FROM tokens WHERE namespc=? ORDER BY timestamp DESC LIMIT 1",
|
||||
(namespace,),
|
||||
|row| row.get(0),
|
||||
|rows| {
|
||||
rows.collect::<std::result::Result<Vec<_>, _>>()
|
||||
.map_err(Into::into)
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn lookup_or_new(
|
||||
context: &Context,
|
||||
namespace: Namespace,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
InBroadcast#Chat#11: My Channel [1 member(s)] Icon: e9b6c7a78aa2e4f415644f55a553e73.png
|
||||
--------------------------------------------------------------------------------
|
||||
Msg#12: info (Contact#Contact#Info): You were invited to join this channel. Waiting for the channel owner's device to reply… [NOTICED][INFO]
|
||||
Msg#13🔒: (Contact#Contact#10): Member Me added by Alice. [FRESH][INFO]
|
||||
Msg#11: info (Contact#Contact#Info): You were invited to join this channel. Waiting for the channel owner's device to reply… [NOTICED][INFO]
|
||||
Msg#12🔒: (Contact#Contact#10): Member Me added by Alice. [FRESH][INFO]
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user