mirror of
https://github.com/chatmail/core.git
synced 2026-05-04 05: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(|| {
|
b.iter(|| {
|
||||||
let mut msg =
|
let (mut msg, _) =
|
||||||
decrypt(encrypted.clone().into_bytes(), &[], black_box(&secrets)).unwrap();
|
decrypt(encrypted.clone().into_bytes(), &[], black_box(&secrets)).unwrap();
|
||||||
let decrypted = msg.as_data_vec().unwrap();
|
let decrypted = msg.as_data_vec().unwrap();
|
||||||
|
|
||||||
@@ -101,7 +101,7 @@ fn criterion_benchmark(c: &mut Criterion) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
b.iter(|| {
|
b.iter(|| {
|
||||||
let mut msg = decrypt(
|
let (mut msg, _) = decrypt(
|
||||||
encrypted.clone().into_bytes(),
|
encrypted.clone().into_bytes(),
|
||||||
&[key_pair.secret.clone()],
|
&[key_pair.secret.clone()],
|
||||||
black_box(&secrets),
|
black_box(&secrets),
|
||||||
|
|||||||
13
src/chat.rs
13
src/chat.rs
@@ -2941,10 +2941,13 @@ async fn prepare_send_msg(
|
|||||||
SystemMessage::MemberRemovedFromGroup | SystemMessage::SecurejoinMessage
|
SystemMessage::MemberRemovedFromGroup | SystemMessage::SecurejoinMessage
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
CantSendReason::MissingKey => msg
|
CantSendReason::MissingKey => {
|
||||||
.param
|
msg.param
|
||||||
.get_bool(Param::ForcePlaintext)
|
.get_bool(Param::ForcePlaintext)
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default()
|
||||||
|
// V2 securejoin messages are symmetrically encrypted, no need for the public key:
|
||||||
|
|| msg.securejoin_step() == Some("vb-request-v2")
|
||||||
|
}
|
||||||
_ => false,
|
_ => false,
|
||||||
};
|
};
|
||||||
if let Some(reason) = chat.why_cant_send_ex(context, &skip_fn).await? {
|
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,
|
chat_id: ChatId,
|
||||||
secret: &str,
|
secret: &str,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
|
info!(context, "Saving broadcast secret for chat {chat_id}");
|
||||||
|
info!(context, "dbg the new secret for chat {chat_id} is {secret}");
|
||||||
context
|
context
|
||||||
.sql
|
.sql
|
||||||
.execute(
|
.execute(
|
||||||
"INSERT INTO broadcasts_shared_secrets (chat_id, secret) VALUES (?, ?)
|
"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),
|
(chat_id, secret),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|||||||
@@ -7,7 +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::securejoin::{get_securejoin_qr, join_securejoin};
|
||||||
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,
|
||||||
@@ -3094,8 +3094,12 @@ async fn test_leave_broadcast_multidevice() -> Result<()> {
|
|||||||
tcm.section("Alice creates broadcast channel with Bob.");
|
tcm.section("Alice creates broadcast channel with Bob.");
|
||||||
let alice_chat_id = create_broadcast(alice, "foo".to_string()).await?;
|
let alice_chat_id = create_broadcast(alice, "foo".to_string()).await?;
|
||||||
let qr = get_securejoin_qr(alice, Some(alice_chat_id)).await.unwrap();
|
let qr = get_securejoin_qr(alice, Some(alice_chat_id)).await.unwrap();
|
||||||
tcm.exec_securejoin_qr(bob0, alice, &qr).await;
|
join_securejoin(bob0, &qr).await.unwrap();
|
||||||
sync(bob0, bob1).await;
|
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.");
|
tcm.section("Alice sends first message to broadcast.");
|
||||||
let sent_msg = alice.send_text(alice_chat_id, "Hello!").await;
|
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.
|
/// 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>(
|
pub fn try_decrypt<'a>(
|
||||||
mail: &'a ParsedMail<'a>,
|
mail: &'a ParsedMail<'a>,
|
||||||
private_keyring: &'a [SignedSecretKey],
|
private_keyring: &'a [SignedSecretKey],
|
||||||
symmetric_secrets: &[String],
|
shared_secrets: &[String],
|
||||||
) -> Result<Option<::pgp::composed::Message<'static>>> {
|
) -> Result<Option<(::pgp::composed::Message<'static>, Option<usize>)>> {
|
||||||
let Some(encrypted_data_part) = get_encrypted_mime(mail) else {
|
let Some(encrypted_data_part) = get_encrypted_mime(mail) else {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
};
|
};
|
||||||
|
|
||||||
let data = encrypted_data_part.get_body_raw()?;
|
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))
|
Ok(Some(msg))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1385,6 +1385,18 @@ impl Message {
|
|||||||
pub fn error(&self) -> Option<String> {
|
pub fn error(&self) -> Option<String> {
|
||||||
self.error.clone()
|
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.
|
/// State of the message.
|
||||||
|
|||||||
@@ -329,7 +329,7 @@ impl MimeFactory {
|
|||||||
|
|
||||||
if let Some(public_key) = public_key_opt {
|
if let Some(public_key) = public_key_opt {
|
||||||
keys.push((addr.clone(), public_key))
|
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());
|
missing_key_addresses.insert(addr.clone());
|
||||||
if is_encrypted {
|
if is_encrypted {
|
||||||
warn!(context, "Missing key for {addr}");
|
warn!(context, "Missing key for {addr}");
|
||||||
@@ -350,7 +350,7 @@ impl MimeFactory {
|
|||||||
|
|
||||||
if let Some(public_key) = public_key_opt {
|
if let Some(public_key) = public_key_opt {
|
||||||
keys.push((addr.clone(), public_key))
|
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());
|
missing_key_addresses.insert(addr.clone());
|
||||||
if is_encrypted {
|
if is_encrypted {
|
||||||
warn!(context, "Missing key for {addr}");
|
warn!(context, "Missing key for {addr}");
|
||||||
@@ -430,7 +430,7 @@ impl MimeFactory {
|
|||||||
|
|
||||||
encryption_keys = if !is_encrypted {
|
encryption_keys = if !is_encrypted {
|
||||||
None
|
None
|
||||||
} else if chat.is_out_broadcast() {
|
} else if should_encrypt_symmetrically(&msg, &chat) {
|
||||||
// Encrypt, but only symmetrically, not with the public keys.
|
// Encrypt, but only symmetrically, not with the public keys.
|
||||||
Some(Vec::new())
|
Some(Vec::new())
|
||||||
} else {
|
} else {
|
||||||
@@ -579,7 +579,7 @@ 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 == "vb-request-v2"
|
||||||
|| step == "vg-member-added"
|
|| step == "vg-member-added"
|
||||||
|| step == "vb-member-added"
|
|| step == "vb-member-added"
|
||||||
|| step == "vc-contact-confirm"
|
|| step == "vc-contact-confirm"
|
||||||
@@ -825,7 +825,7 @@ impl MimeFactory {
|
|||||||
} else if let Loaded::Message { msg, .. } = &self.loaded {
|
} else if let Loaded::Message { msg, .. } = &self.loaded {
|
||||||
if msg.param.get_cmd() == SystemMessage::SecurejoinMessage {
|
if msg.param.get_cmd() == SystemMessage::SecurejoinMessage {
|
||||||
let step = msg.param.get(Param::Arg).unwrap_or_default();
|
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((
|
headers.push((
|
||||||
"Auto-Submitted",
|
"Auto-Submitted",
|
||||||
mail_builder::headers::raw::Raw::new("auto-replied".to_string()).into(),
|
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 {
|
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
|
// 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),
|
||||||
@@ -1196,6 +1202,7 @@ impl MimeFactory {
|
|||||||
|
|
||||||
let encrypted = if let Some(symmetric_key) = symmetric_key {
|
let encrypted = if let Some(symmetric_key) = symmetric_key {
|
||||||
info!(context, "Symmetrically encrypting for broadcast channel.");
|
info!(context, "Symmetrically encrypting for broadcast channel.");
|
||||||
|
info!(context, "secret: {symmetric_key}"); // TODO
|
||||||
encrypt_helper
|
encrypt_helper
|
||||||
.encrypt_for_broadcast(context, &symmetric_key, message, compress)
|
.encrypt_for_broadcast(context, &symmetric_key, message, compress)
|
||||||
.await?
|
.await?
|
||||||
@@ -1547,7 +1554,7 @@ impl MimeFactory {
|
|||||||
headers.push((
|
headers.push((
|
||||||
if step == "vg-request-with-auth"
|
if step == "vg-request-with-auth"
|
||||||
|| step == "vc-request-with-auth"
|
|| step == "vc-request-with-auth"
|
||||||
|| step == "vb-request-with-auth"
|
|| step == "vb-request-v2"
|
||||||
{
|
{
|
||||||
"Secure-Join-Auth"
|
"Secure-Join-Auth"
|
||||||
} else {
|
} else {
|
||||||
@@ -1898,6 +1905,22 @@ fn hidden_recipients() -> Address<'static> {
|
|||||||
Address::new_group(Some("hidden-recipients".to_string()), Vec::new())
|
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>> {
|
async fn build_body_file(context: &Context, msg: &Message) -> Result<MimePart<'static>> {
|
||||||
let file_name = msg.get_filename().context("msg has no file")?;
|
let file_name = msg.get_filename().context("msg has no file")?;
|
||||||
let blob = msg
|
let blob = msg
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ use crate::authres::handle_authres;
|
|||||||
use crate::blob::BlobObject;
|
use crate::blob::BlobObject;
|
||||||
use crate::chat::ChatId;
|
use crate::chat::ChatId;
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::constants;
|
|
||||||
use crate::contact::ContactId;
|
use crate::contact::ContactId;
|
||||||
use crate::context::Context;
|
use crate::context::Context;
|
||||||
use crate::decrypt::{try_decrypt, validate_detached_signature};
|
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,
|
get_filemeta, parse_receive_headers, smeared_time, time, truncate_msg_text, validate_id,
|
||||||
};
|
};
|
||||||
use crate::{chatlist_events, location, stock_str, tools};
|
use crate::{chatlist_events, location, stock_str, tools};
|
||||||
|
use crate::{constants, token};
|
||||||
|
|
||||||
/// A parsed MIME message.
|
/// 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
|
/// Sender timestamp in secs since epoch. Allowed to be in the future due to unsynchronized
|
||||||
/// clocks, but not too much.
|
/// clocks, but not too much.
|
||||||
pub(crate) timestamp_sent: i64,
|
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)]
|
#[derive(Debug, PartialEq)]
|
||||||
@@ -234,6 +238,25 @@ pub enum SystemMessage {
|
|||||||
CallEnded = 67,
|
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";
|
const MIME_AC_SETUP_FILE: &str = "application/autocrypt-setup";
|
||||||
|
|
||||||
impl MimeMessage {
|
impl MimeMessage {
|
||||||
@@ -354,7 +377,7 @@ impl MimeMessage {
|
|||||||
|
|
||||||
let mail_raw; // Memory location for a possible decrypted message.
|
let mail_raw; // Memory location for a possible decrypted message.
|
||||||
let decrypted_msg; // Decrypted signed OpenPGP message.
|
let decrypted_msg; // Decrypted signed OpenPGP message.
|
||||||
let symmetric_secrets: Vec<String> = context
|
let mut secrets: Vec<String> = context
|
||||||
.sql
|
.sql
|
||||||
.query_map(
|
.query_map(
|
||||||
"SELECT secret FROM broadcasts_shared_secrets",
|
"SELECT secret FROM broadcasts_shared_secrets",
|
||||||
@@ -366,11 +389,13 @@ impl MimeMessage {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await?;
|
.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(|| {
|
let (mail, is_encrypted, decrypted_with) = match tokio::task::block_in_place(|| {
|
||||||
try_decrypt(&mail, &private_keyring, &symmetric_secrets)
|
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();
|
mail_raw = msg.as_data_vec().unwrap_or_default();
|
||||||
|
|
||||||
let decrypted_mail = mailparse::parse_mail(&mail_raw)?;
|
let decrypted_mail = mailparse::parse_mail(&mail_raw)?;
|
||||||
@@ -397,18 +422,29 @@ impl MimeMessage {
|
|||||||
aheader_value = Some(protected_aheader_value);
|
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) => {
|
Ok(None) => {
|
||||||
mail_raw = Vec::new();
|
mail_raw = Vec::new();
|
||||||
decrypted_msg = None;
|
decrypted_msg = None;
|
||||||
(Ok(mail), false)
|
(Ok(mail), false, EncryptedWith::None)
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
mail_raw = Vec::new();
|
mail_raw = Vec::new();
|
||||||
decrypted_msg = None;
|
decrypted_msg = None;
|
||||||
warn!(context, "decryption failed: {:#}", err);
|
warn!(context, "decryption failed: {:#}", err);
|
||||||
(Err(err), false)
|
(Err(err), false, EncryptedWith::None)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -620,6 +656,7 @@ impl MimeMessage {
|
|||||||
is_bot: None,
|
is_bot: None,
|
||||||
timestamp_rcvd,
|
timestamp_rcvd,
|
||||||
timestamp_sent,
|
timestamp_sent,
|
||||||
|
was_encrypted_with: decrypted_with,
|
||||||
};
|
};
|
||||||
|
|
||||||
match partial {
|
match partial {
|
||||||
|
|||||||
@@ -103,6 +103,9 @@ pub enum Param {
|
|||||||
/// removed from the group.
|
/// removed from the group.
|
||||||
///
|
///
|
||||||
/// For "MemberAddedToGroup" this is the email address added to 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',
|
Arg = b'E',
|
||||||
|
|
||||||
/// For Messages
|
/// For Messages
|
||||||
@@ -111,6 +114,9 @@ pub enum Param {
|
|||||||
///
|
///
|
||||||
/// For `BobHandshakeMsg::RequestWithAuth`, this is the `Secure-Join-Auth` header.
|
/// 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::MultiDeviceSync`], this contains the ids that are synced.
|
||||||
///
|
///
|
||||||
/// For [`SystemMessage::MemberAddedToGroup`],
|
/// For [`SystemMessage::MemberAddedToGroup`],
|
||||||
|
|||||||
32
src/pgp.rs
32
src/pgp.rs
@@ -8,7 +8,7 @@ use chrono::SubsecRound;
|
|||||||
use deltachat_contact_tools::EmailAddress;
|
use deltachat_contact_tools::EmailAddress;
|
||||||
use pgp::armor::BlockType;
|
use pgp::armor::BlockType;
|
||||||
use pgp::composed::{
|
use pgp::composed::{
|
||||||
ArmorOptions, Deserializable, KeyType as PgpKeyType, Message, MessageBuilder,
|
ArmorOptions, Deserializable, InnerRingResult, KeyType as PgpKeyType, Message, MessageBuilder,
|
||||||
SecretKeyParamsBuilder, SignedPublicKey, SignedPublicSubKey, SignedSecretKey,
|
SecretKeyParamsBuilder, SignedPublicKey, SignedPublicSubKey, SignedSecretKey,
|
||||||
StandaloneSignature, SubkeyParamsBuilder, TheRing,
|
StandaloneSignature, SubkeyParamsBuilder, TheRing,
|
||||||
};
|
};
|
||||||
@@ -239,13 +239,19 @@ pub fn pk_calc_signature(
|
|||||||
|
|
||||||
/// Decrypts the message with keys from the private key keyring.
|
/// Decrypts the message with keys from the private key keyring.
|
||||||
///
|
///
|
||||||
/// Receiver private keys are provided in
|
/// Receiver private keys are passed in `private_keys_for_decryption`,
|
||||||
/// `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(
|
pub fn decrypt(
|
||||||
ctext: Vec<u8>,
|
ctext: Vec<u8>,
|
||||||
private_keys_for_decryption: &[SignedSecretKey],
|
private_keys_for_decryption: &[SignedSecretKey],
|
||||||
symmetric_secrets: &[String],
|
shared_secrets: &[String],
|
||||||
) -> Result<pgp::composed::Message<'static>> {
|
) -> Result<(pgp::composed::Message<'static>, Option<usize>)> {
|
||||||
let cursor = Cursor::new(ctext);
|
let cursor = Cursor::new(ctext);
|
||||||
let (msg, _headers) = Message::from_armor(cursor)?;
|
let (msg, _headers) = Message::from_armor(cursor)?;
|
||||||
|
|
||||||
@@ -253,7 +259,7 @@ pub fn decrypt(
|
|||||||
let empty_pw = Password::empty();
|
let empty_pw = Password::empty();
|
||||||
|
|
||||||
// TODO it may degrade performance that we always try out all passwords here
|
// 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()
|
.iter()
|
||||||
.map(|p| Password::from(p.as_str()))
|
.map(|p| Password::from(p.as_str()))
|
||||||
.collect();
|
.collect();
|
||||||
@@ -266,12 +272,17 @@ pub fn decrypt(
|
|||||||
session_keys: vec![],
|
session_keys: vec![],
|
||||||
allow_legacy: false,
|
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
|
// remove one layer of compression
|
||||||
let msg = msg.decompress()?;
|
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
|
/// Returns fingerprints
|
||||||
@@ -407,7 +418,7 @@ mod tests {
|
|||||||
HashSet<Fingerprint>,
|
HashSet<Fingerprint>,
|
||||||
Vec<u8>,
|
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 content = msg.as_data_vec()?;
|
||||||
let ret_signature_fingerprints =
|
let ret_signature_fingerprints =
|
||||||
valid_signature_fingerprints(&msg, public_keys_for_validation);
|
valid_signature_fingerprints(&msg, public_keys_for_validation);
|
||||||
@@ -611,13 +622,14 @@ mod tests {
|
|||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let bob_private_keyring = crate::key::load_self_secret_keyring(bob).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(),
|
ctext.into(),
|
||||||
&bob_private_keyring,
|
&bob_private_keyring,
|
||||||
&[shared_secret.to_string()],
|
&[shared_secret.to_string()],
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
assert_eq!(decrypted.as_data_vec()?, plain);
|
assert_eq!(decrypted.as_data_vec()?, plain);
|
||||||
|
assert_eq!(index_of_secret, Some(0));
|
||||||
|
|
||||||
Ok(())
|
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
|
// -> 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!(
|
if !matches!(
|
||||||
step,
|
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 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();
|
||||||
@@ -367,7 +367,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" | "vb-request-with-auth" => {
|
"vg-request-with-auth" | "vc-request-with-auth" | "vb-request-v2" => {
|
||||||
/*==========================================================
|
/*==========================================================
|
||||||
==== Alice - the inviter side ====
|
==== Alice - the inviter side ====
|
||||||
==== Steps 5+6 in "Setup verified contact" protocol ====
|
==== Steps 5+6 in "Setup verified contact" protocol ====
|
||||||
@@ -390,8 +390,12 @@ pub(crate) async fn handle_securejoin_handshake(
|
|||||||
);
|
);
|
||||||
return Ok(HandshakeMessage::Ignore);
|
return Ok(HandshakeMessage::Ignore);
|
||||||
}
|
}
|
||||||
// verify that the `Secure-Join-Auth:`-header matches the secret written to the QR code
|
// 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 {
|
// 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!(
|
warn!(
|
||||||
context,
|
context,
|
||||||
"Ignoring {step} message because of missing auth code."
|
"Ignoring {step} message because of missing auth code."
|
||||||
@@ -447,9 +451,16 @@ pub(crate) async fn handle_securejoin_handshake(
|
|||||||
.await?;
|
.await?;
|
||||||
inviter_progress(context, contact_id, 800);
|
inviter_progress(context, contact_id, 800);
|
||||||
inviter_progress(context, contact_id, 1000);
|
inviter_progress(context, contact_id, 1000);
|
||||||
|
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
|
// 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.
|
// member twice. Another device will know the member's key from Autocrypt-Gossip.
|
||||||
Ok(HandshakeMessage::Done)
|
Ok(HandshakeMessage::Done)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Setup verified contact.
|
// Setup verified contact.
|
||||||
secure_connection_established(
|
secure_connection_established(
|
||||||
@@ -594,7 +605,7 @@ pub(crate) async fn observe_securejoin_on_other_device(
|
|||||||
inviter_progress(context, contact_id, 1000);
|
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??
|
// 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" {
|
if step == "vg-request-with-auth" || step == "vc-request-with-auth" {
|
||||||
// This actually reflects what happens on the first device (which does the secure
|
// 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.
|
//! 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::HandshakeMessage;
|
||||||
use super::qrinvite::QrInvite;
|
use super::qrinvite::QrInvite;
|
||||||
use crate::chat::{
|
use crate::chat::{self, ChatId, ProtectionStatus, is_contact_in_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;
|
||||||
use crate::events::EventType;
|
use crate::events::EventType;
|
||||||
use crate::key::self_fingerprint;
|
use crate::key::self_fingerprint;
|
||||||
use crate::log::{LogExt as _, info};
|
use crate::log::info;
|
||||||
use crate::message::{Message, Viewtype};
|
use crate::message::{Message, Viewtype};
|
||||||
use crate::mimeparser::{MimeMessage, SystemMessage};
|
use crate::mimeparser::{MimeMessage, SystemMessage};
|
||||||
use crate::param::Param;
|
use crate::param::Param;
|
||||||
@@ -51,63 +49,48 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul
|
|||||||
QrInvite::Group { .. } => Blocked::Yes,
|
QrInvite::Group { .. } => Blocked::Yes,
|
||||||
QrInvite::Broadcast { .. } => Blocked::Yes,
|
QrInvite::Broadcast { .. } => Blocked::Yes,
|
||||||
};
|
};
|
||||||
let chat_id = ChatId::create_for_contact_with_blocked(context, invite.contact_id(), hidden)
|
|
||||||
|
// The 1:1 chat with the inviter
|
||||||
|
let private_chat_id =
|
||||||
|
ChatId::create_for_contact_with_blocked(context, invite.contact_id(), hidden)
|
||||||
.await
|
.await
|
||||||
.with_context(|| format!("can't create chat for contact {}", invite.contact_id()))?;
|
.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?;
|
ContactId::scaleup_origin(context, &[invite.contact_id()], Origin::SecurejoinJoined).await?;
|
||||||
context.emit_event(EventType::ContactsChanged(None));
|
context.emit_event(EventType::ContactsChanged(None));
|
||||||
|
|
||||||
if let QrInvite::Broadcast {
|
if invite.is_v2() {
|
||||||
shared_secret,
|
if !verify_sender_by_fingerprint(context, invite.fingerprint(), invite.contact_id()).await?
|
||||||
grpid,
|
|
||||||
broadcast_name,
|
|
||||||
..
|
|
||||||
} = &invite
|
|
||||||
{
|
{
|
||||||
// TODO this causes some performance penalty because joining_chat_id is used again below,
|
bail!("V2 protocol failed because of fingerprint mismatch");
|
||||||
// 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");
|
info!(context, "Using fast securejoin with symmetric encryption");
|
||||||
|
|
||||||
// The message has to be sent into the broadcast chat, rather than the 1:1 chat,
|
let mut msg = Message {
|
||||||
// so that it will be symmetrically encrypted
|
viewtype: Viewtype::Text,
|
||||||
send_handshake_message(
|
text: "Secure-Join: vb-request-v2".to_string(),
|
||||||
context,
|
hidden: true,
|
||||||
&invite,
|
..Default::default()
|
||||||
broadcast_chat_id,
|
};
|
||||||
BobHandshakeMsg::RequestWithAuth,
|
msg.param.set_cmd(SystemMessage::SecurejoinMessage);
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// Mark 1:1 chat as verified already.
|
msg.param.set(Param::Arg, "vb-request-v2");
|
||||||
chat_id
|
msg.param.set(Param::Arg2, invite.authcode());
|
||||||
.set_protection(
|
msg.param.set_int(Param::GuaranteeE2ee, 1);
|
||||||
context,
|
let bob_fp = self_fingerprint(context).await?;
|
||||||
ProtectionStatus::Protected,
|
msg.param.set(Param::Arg3, bob_fp);
|
||||||
time(),
|
|
||||||
Some(invite.contact_id()),
|
chat::send_msg(context, private_chat_id, &mut msg).await?;
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
context.emit_event(EventType::SecurejoinJoinerProgress {
|
context.emit_event(EventType::SecurejoinJoinerProgress {
|
||||||
contact_id: invite.contact_id(),
|
contact_id: invite.contact_id(),
|
||||||
progress: JoinerProgress::RequestWithAuthSent.to_usize(),
|
progress: JoinerProgress::RequestWithAuthSent.to_usize(),
|
||||||
});
|
});
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Start the original (non-broadcast) protocol and initialise the state.
|
// Start the version 1 protocol and initialise the state.
|
||||||
let has_key = context
|
let has_key = context
|
||||||
.sql
|
.sql
|
||||||
.exists(
|
.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.
|
// The scanned fingerprint matches Alice's key, we can proceed to step 4b.
|
||||||
info!(context, "Taking securejoin protocol shortcut");
|
info!(context, "Taking securejoin protocol shortcut");
|
||||||
send_handshake_message(context, &invite, chat_id, BobHandshakeMsg::RequestWithAuth)
|
send_handshake_message(
|
||||||
|
context,
|
||||||
|
&invite,
|
||||||
|
private_chat_id,
|
||||||
|
BobHandshakeMsg::RequestWithAuth,
|
||||||
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// Mark 1:1 chat as verified already.
|
// Mark 1:1 chat as verified already.
|
||||||
chat_id
|
private_chat_id
|
||||||
.set_protection(
|
.set_protection(
|
||||||
context,
|
context,
|
||||||
ProtectionStatus::Protected,
|
ProtectionStatus::Protected,
|
||||||
@@ -140,9 +128,10 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul
|
|||||||
progress: JoinerProgress::RequestWithAuthSent.to_usize(),
|
progress: JoinerProgress::RequestWithAuthSent.to_usize(),
|
||||||
});
|
});
|
||||||
} else {
|
} 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 { .. } => {
|
QrInvite::Group { .. } => {
|
||||||
// For a secure-join we need to create the group and add the contact. The group will
|
// 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.
|
// 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, joining_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,
|
joining_chat_id,
|
||||||
&[invite.contact_id()],
|
&[invite.contact_id()],
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
let msg = stock_str::secure_join_started(context, 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?;
|
chat::add_info_msg(context, joining_chat_id, &msg, time()).await?;
|
||||||
Ok(group_chat_id)
|
Ok(joining_chat_id)
|
||||||
}
|
}
|
||||||
QrInvite::Broadcast { .. } => {
|
QrInvite::Broadcast { .. } => {
|
||||||
// TODO code duplication with previous block
|
// TODO code duplication with previous block
|
||||||
let broadcast_chat_id = joining_chat_id(context, &invite, chat_id).await?;
|
if !is_contact_in_chat(context, joining_chat_id, invite.contact_id()).await? {
|
||||||
if !is_contact_in_chat(context, broadcast_chat_id, invite.contact_id()).await? {
|
|
||||||
chat::add_to_chat_contacts_table(
|
chat::add_to_chat_contacts_table(
|
||||||
context,
|
context,
|
||||||
time(),
|
time(),
|
||||||
broadcast_chat_id,
|
joining_chat_id,
|
||||||
&[invite.contact_id()],
|
&[invite.contact_id()],
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -179,8 +166,8 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul
|
|||||||
|
|
||||||
// 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, broadcast_chat_id, msg, time()).await?;
|
chat::add_info_msg(context, joining_chat_id, msg, time()).await?;
|
||||||
Ok(broadcast_chat_id)
|
Ok(joining_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
|
||||||
@@ -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.
|
// race with its change, we don't add our message below the protection message.
|
||||||
let sort_to_bottom = true;
|
let sort_to_bottom = true;
|
||||||
let (received, incoming) = (false, false);
|
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)
|
.calc_sort_timestamp(context, 0, sort_to_bottom, received, incoming)
|
||||||
.await?;
|
.await?;
|
||||||
if chat_id.is_protected(context).await? == ProtectionStatus::Unprotected {
|
if private_chat_id.is_protected(context).await? == ProtectionStatus::Unprotected {
|
||||||
let ts_start = time();
|
let ts_start = time();
|
||||||
chat::add_info_msg_with_cmd(
|
chat::add_info_msg_with_cmd(
|
||||||
context,
|
context,
|
||||||
chat_id,
|
private_chat_id,
|
||||||
&stock_str::securejoin_wait(context).await,
|
&stock_str::securejoin_wait(context).await,
|
||||||
SystemMessage::SecurejoinWait,
|
SystemMessage::SecurejoinWait,
|
||||||
ts_sort,
|
ts_sort,
|
||||||
@@ -207,7 +194,7 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
Ok(chat_id)
|
Ok(private_chat_id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -334,7 +321,7 @@ pub(crate) async fn send_handshake_message(
|
|||||||
match step {
|
match step {
|
||||||
BobHandshakeMsg::Request => {
|
BobHandshakeMsg::Request => {
|
||||||
// Sends the Secure-Join-Invitenumber header in mimefactory.rs.
|
// 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();
|
msg.force_plaintext();
|
||||||
}
|
}
|
||||||
BobHandshakeMsg::RequestWithAuth => {
|
BobHandshakeMsg::RequestWithAuth => {
|
||||||
@@ -368,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, vg-request-with-auth, or vb-request-with-auth
|
/// vc-request-with-auth, vg-request-with-auth, or vb-request-v2
|
||||||
RequestWithAuth,
|
RequestWithAuth,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -399,7 +386,9 @@ impl BobHandshakeMsg {
|
|||||||
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 { .. } => "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 {
|
||||||
contact_id: ContactId,
|
contact_id: ContactId,
|
||||||
fingerprint: Fingerprint,
|
fingerprint: Fingerprint,
|
||||||
invitenumber: String,
|
invitenumber: Option<String>,
|
||||||
authcode: String,
|
authcode: String,
|
||||||
},
|
},
|
||||||
Group {
|
Group {
|
||||||
@@ -26,7 +26,7 @@ pub enum QrInvite {
|
|||||||
fingerprint: Fingerprint,
|
fingerprint: Fingerprint,
|
||||||
name: String,
|
name: String,
|
||||||
grpid: String,
|
grpid: String,
|
||||||
invitenumber: String,
|
invitenumber: Option<String>,
|
||||||
authcode: String,
|
authcode: String,
|
||||||
},
|
},
|
||||||
Broadcast {
|
Broadcast {
|
||||||
@@ -62,10 +62,12 @@ impl QrInvite {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The `INVITENUMBER` of the setup-contact/secure-join protocol.
|
/// The `INVITENUMBER` of the setup-contact/secure-join protocol.
|
||||||
pub fn invitenumber(&self) -> &str {
|
pub fn invitenumber(&self) -> Option<&str> {
|
||||||
match self {
|
match self {
|
||||||
Self::Contact { invitenumber, .. } | Self::Group { invitenumber, .. } => invitenumber,
|
Self::Contact { invitenumber, .. } | Self::Group { invitenumber, .. } => {
|
||||||
Self::Broadcast { .. } => panic!("broadcast invite has no invite number"), // TODO panic
|
invitenumber.as_deref()
|
||||||
|
}
|
||||||
|
Self::Broadcast { .. } => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,6 +79,17 @@ impl QrInvite {
|
|||||||
| Self::Broadcast { authcode, .. } => authcode,
|
| 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 {
|
impl TryFrom<Qr> for QrInvite {
|
||||||
@@ -92,7 +105,7 @@ impl TryFrom<Qr> for QrInvite {
|
|||||||
} => Ok(QrInvite::Contact {
|
} => Ok(QrInvite::Contact {
|
||||||
contact_id,
|
contact_id,
|
||||||
fingerprint,
|
fingerprint,
|
||||||
invitenumber,
|
invitenumber: Some(invitenumber),
|
||||||
authcode,
|
authcode,
|
||||||
}),
|
}),
|
||||||
Qr::AskVerifyGroup {
|
Qr::AskVerifyGroup {
|
||||||
@@ -107,7 +120,7 @@ impl TryFrom<Qr> for QrInvite {
|
|||||||
fingerprint,
|
fingerprint,
|
||||||
name: grpname,
|
name: grpname,
|
||||||
grpid,
|
grpid,
|
||||||
invitenumber,
|
invitenumber: Some(invitenumber),
|
||||||
authcode,
|
authcode,
|
||||||
}),
|
}),
|
||||||
Qr::AskJoinBroadcast {
|
Qr::AskJoinBroadcast {
|
||||||
|
|||||||
15
src/token.rs
15
src/token.rs
@@ -61,6 +61,21 @@ pub async fn lookup(
|
|||||||
.await
|
.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(
|
pub async fn lookup_or_new(
|
||||||
context: &Context,
|
context: &Context,
|
||||||
namespace: Namespace,
|
namespace: Namespace,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
InBroadcast#Chat#11: My Channel [1 member(s)] Icon: e9b6c7a78aa2e4f415644f55a553e73.png
|
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#11: 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#12🔒: (Contact#Contact#10): Member Me added by Alice. [FRESH][INFO]
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user