mirror of
https://github.com/chatmail/core.git
synced 2026-04-24 08:56:29 +03:00
feat: support receiving Autocrypt-Gossip with _verified attribute
This commit is a preparation for sending Autocrypt-Gossip with `_verified` attribute instead of `Chat-Verified` header.
This commit is contained in:
@@ -46,6 +46,13 @@ pub struct Aheader {
|
|||||||
pub addr: String,
|
pub addr: String,
|
||||||
pub public_key: SignedPublicKey,
|
pub public_key: SignedPublicKey,
|
||||||
pub prefer_encrypt: EncryptPreference,
|
pub prefer_encrypt: EncryptPreference,
|
||||||
|
|
||||||
|
// Whether `_verified` attribute is present.
|
||||||
|
//
|
||||||
|
// `_verified` attribute is an extension to `Autocrypt-Gossip`
|
||||||
|
// header that is used to tell that the sender
|
||||||
|
// marked this key as verified.
|
||||||
|
pub verified: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for Aheader {
|
impl fmt::Display for Aheader {
|
||||||
@@ -54,6 +61,9 @@ impl fmt::Display for Aheader {
|
|||||||
if self.prefer_encrypt == EncryptPreference::Mutual {
|
if self.prefer_encrypt == EncryptPreference::Mutual {
|
||||||
write!(fmt, " prefer-encrypt=mutual;")?;
|
write!(fmt, " prefer-encrypt=mutual;")?;
|
||||||
}
|
}
|
||||||
|
if self.verified {
|
||||||
|
write!(fmt, " _verified=1;")?;
|
||||||
|
}
|
||||||
|
|
||||||
// adds a whitespace every 78 characters, this allows
|
// adds a whitespace every 78 characters, this allows
|
||||||
// email crate to wrap the lines according to RFC 5322
|
// email crate to wrap the lines according to RFC 5322
|
||||||
@@ -108,6 +118,8 @@ impl FromStr for Aheader {
|
|||||||
.and_then(|raw| raw.parse().ok())
|
.and_then(|raw| raw.parse().ok())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let verified = attributes.remove("_verified").is_some();
|
||||||
|
|
||||||
// Autocrypt-Level0: unknown attributes starting with an underscore can be safely ignored
|
// Autocrypt-Level0: unknown attributes starting with an underscore can be safely ignored
|
||||||
// Autocrypt-Level0: unknown attribute, treat the header as invalid
|
// Autocrypt-Level0: unknown attribute, treat the header as invalid
|
||||||
if attributes.keys().any(|k| !k.starts_with('_')) {
|
if attributes.keys().any(|k| !k.starts_with('_')) {
|
||||||
@@ -118,6 +130,7 @@ impl FromStr for Aheader {
|
|||||||
addr,
|
addr,
|
||||||
public_key,
|
public_key,
|
||||||
prefer_encrypt,
|
prefer_encrypt,
|
||||||
|
verified,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -135,6 +148,7 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(h.addr, "me@mail.com");
|
assert_eq!(h.addr, "me@mail.com");
|
||||||
assert_eq!(h.prefer_encrypt, EncryptPreference::Mutual);
|
assert_eq!(h.prefer_encrypt, EncryptPreference::Mutual);
|
||||||
|
assert_eq!(h.verified, false);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,7 +245,8 @@ mod tests {
|
|||||||
Aheader {
|
Aheader {
|
||||||
addr: "test@example.com".to_string(),
|
addr: "test@example.com".to_string(),
|
||||||
public_key: SignedPublicKey::from_base64(RAWKEY).unwrap(),
|
public_key: SignedPublicKey::from_base64(RAWKEY).unwrap(),
|
||||||
prefer_encrypt: EncryptPreference::Mutual
|
prefer_encrypt: EncryptPreference::Mutual,
|
||||||
|
verified: false
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.contains("prefer-encrypt=mutual;")
|
.contains("prefer-encrypt=mutual;")
|
||||||
@@ -246,7 +261,8 @@ mod tests {
|
|||||||
Aheader {
|
Aheader {
|
||||||
addr: "test@example.com".to_string(),
|
addr: "test@example.com".to_string(),
|
||||||
public_key: SignedPublicKey::from_base64(RAWKEY).unwrap(),
|
public_key: SignedPublicKey::from_base64(RAWKEY).unwrap(),
|
||||||
prefer_encrypt: EncryptPreference::NoPreference
|
prefer_encrypt: EncryptPreference::NoPreference,
|
||||||
|
verified: false
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.contains("prefer-encrypt")
|
.contains("prefer-encrypt")
|
||||||
@@ -259,10 +275,24 @@ mod tests {
|
|||||||
Aheader {
|
Aheader {
|
||||||
addr: "TeSt@eXaMpLe.cOm".to_string(),
|
addr: "TeSt@eXaMpLe.cOm".to_string(),
|
||||||
public_key: SignedPublicKey::from_base64(RAWKEY).unwrap(),
|
public_key: SignedPublicKey::from_base64(RAWKEY).unwrap(),
|
||||||
prefer_encrypt: EncryptPreference::Mutual
|
prefer_encrypt: EncryptPreference::Mutual,
|
||||||
|
verified: false
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.contains("test@example.com")
|
.contains("test@example.com")
|
||||||
);
|
);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
format!(
|
||||||
|
"{}",
|
||||||
|
Aheader {
|
||||||
|
addr: "test@example.com".to_string(),
|
||||||
|
public_key: SignedPublicKey::from_base64(RAWKEY).unwrap(),
|
||||||
|
prefer_encrypt: EncryptPreference::NoPreference,
|
||||||
|
verified: true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.contains("_verified")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ impl EncryptHelper {
|
|||||||
addr: self.addr.clone(),
|
addr: self.addr.clone(),
|
||||||
public_key: self.public_key.clone(),
|
public_key: self.public_key.clone(),
|
||||||
prefer_encrypt: self.prefer_encrypt,
|
prefer_encrypt: self.prefer_encrypt,
|
||||||
|
verified: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1098,6 +1098,7 @@ impl MimeFactory {
|
|||||||
// Autocrypt 1.1.0 specification says that
|
// Autocrypt 1.1.0 specification says that
|
||||||
// `prefer-encrypt` attribute SHOULD NOT be included.
|
// `prefer-encrypt` attribute SHOULD NOT be included.
|
||||||
prefer_encrypt: EncryptPreference::NoPreference,
|
prefer_encrypt: EncryptPreference::NoPreference,
|
||||||
|
verified: false,
|
||||||
}
|
}
|
||||||
.to_string();
|
.to_string();
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
//! # MIME message parsing module.
|
//! # MIME message parsing module.
|
||||||
|
|
||||||
use std::cmp::min;
|
use std::cmp::min;
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{BTreeMap, HashMap, HashSet};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::str;
|
use std::str;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
@@ -36,6 +36,17 @@ use crate::tools::{
|
|||||||
};
|
};
|
||||||
use crate::{chatlist_events, location, stock_str, tools};
|
use crate::{chatlist_events, location, stock_str, tools};
|
||||||
|
|
||||||
|
/// Public key extracted from `Autocrypt-Gossip`
|
||||||
|
/// header with associated information.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct GossipedKey {
|
||||||
|
/// Public key extracted from `keydata` attribute.
|
||||||
|
pub public_key: SignedPublicKey,
|
||||||
|
|
||||||
|
/// True if `Autocrypt-Gossip` has a `_verified` attribute.
|
||||||
|
pub verified: bool,
|
||||||
|
}
|
||||||
|
|
||||||
/// A parsed MIME message.
|
/// A parsed MIME message.
|
||||||
///
|
///
|
||||||
/// This represents the relevant information of a parsed MIME message
|
/// This represents the relevant information of a parsed MIME message
|
||||||
@@ -85,7 +96,7 @@ pub(crate) struct MimeMessage {
|
|||||||
|
|
||||||
/// The addresses for which there was a gossip header
|
/// The addresses for which there was a gossip header
|
||||||
/// and their respective gossiped keys.
|
/// and their respective gossiped keys.
|
||||||
pub gossiped_keys: HashMap<String, SignedPublicKey>,
|
pub gossiped_keys: BTreeMap<String, GossipedKey>,
|
||||||
|
|
||||||
/// Fingerprint of the key in the Autocrypt header.
|
/// Fingerprint of the key in the Autocrypt header.
|
||||||
///
|
///
|
||||||
@@ -1963,9 +1974,9 @@ async fn parse_gossip_headers(
|
|||||||
from: &str,
|
from: &str,
|
||||||
recipients: &[SingleInfo],
|
recipients: &[SingleInfo],
|
||||||
gossip_headers: Vec<String>,
|
gossip_headers: Vec<String>,
|
||||||
) -> Result<HashMap<String, SignedPublicKey>> {
|
) -> Result<BTreeMap<String, GossipedKey>> {
|
||||||
// XXX split the parsing from the modification part
|
// XXX split the parsing from the modification part
|
||||||
let mut gossiped_keys: HashMap<String, SignedPublicKey> = Default::default();
|
let mut gossiped_keys: BTreeMap<String, GossipedKey> = Default::default();
|
||||||
|
|
||||||
for value in &gossip_headers {
|
for value in &gossip_headers {
|
||||||
let header = match value.parse::<Aheader>() {
|
let header = match value.parse::<Aheader>() {
|
||||||
@@ -2007,7 +2018,12 @@ async fn parse_gossip_headers(
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
gossiped_keys.insert(header.addr.to_lowercase(), header.public_key);
|
let gossiped_key = GossipedKey {
|
||||||
|
public_key: header.public_key,
|
||||||
|
|
||||||
|
verified: header.verified,
|
||||||
|
};
|
||||||
|
gossiped_keys.insert(header.addr.to_lowercase(), gossiped_key);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(gossiped_keys)
|
Ok(gossiped_keys)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
//! Internet Message Format reception pipeline.
|
//! Internet Message Format reception pipeline.
|
||||||
|
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{BTreeMap, HashSet};
|
||||||
use std::iter;
|
use std::iter;
|
||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
@@ -28,14 +28,14 @@ use crate::events::EventType;
|
|||||||
use crate::headerdef::{HeaderDef, HeaderDefMap};
|
use crate::headerdef::{HeaderDef, HeaderDefMap};
|
||||||
use crate::imap::{GENERATED_PREFIX, markseen_on_imap_table};
|
use crate::imap::{GENERATED_PREFIX, markseen_on_imap_table};
|
||||||
use crate::key::self_fingerprint_opt;
|
use crate::key::self_fingerprint_opt;
|
||||||
use crate::key::{DcKey, Fingerprint, SignedPublicKey};
|
use crate::key::{DcKey, Fingerprint};
|
||||||
use crate::log::LogExt;
|
use crate::log::LogExt;
|
||||||
use crate::log::{info, warn};
|
use crate::log::{info, warn};
|
||||||
use crate::logged_debug_assert;
|
use crate::logged_debug_assert;
|
||||||
use crate::message::{
|
use crate::message::{
|
||||||
self, Message, MessageState, MessengerMessage, MsgId, Viewtype, rfc724_mid_exists,
|
self, Message, MessageState, MessengerMessage, MsgId, Viewtype, rfc724_mid_exists,
|
||||||
};
|
};
|
||||||
use crate::mimeparser::{AvatarAction, MimeMessage, SystemMessage, parse_message_ids};
|
use crate::mimeparser::{AvatarAction, GossipedKey, MimeMessage, SystemMessage, parse_message_ids};
|
||||||
use crate::param::{Param, Params};
|
use crate::param::{Param, Params};
|
||||||
use crate::peer_channels::{add_gossip_peer_from_header, insert_topic_stub};
|
use crate::peer_channels::{add_gossip_peer_from_header, insert_topic_stub};
|
||||||
use crate::reaction::{Reaction, set_msg_reaction};
|
use crate::reaction::{Reaction, set_msg_reaction};
|
||||||
@@ -835,7 +835,7 @@ pub(crate) async fn receive_imf_inner(
|
|||||||
context
|
context
|
||||||
.sql
|
.sql
|
||||||
.transaction(move |transaction| {
|
.transaction(move |transaction| {
|
||||||
let fingerprint = gossiped_key.dc_fingerprint().hex();
|
let fingerprint = gossiped_key.public_key.dc_fingerprint().hex();
|
||||||
transaction.execute(
|
transaction.execute(
|
||||||
"INSERT INTO gossip_timestamp (chat_id, fingerprint, timestamp)
|
"INSERT INTO gossip_timestamp (chat_id, fingerprint, timestamp)
|
||||||
VALUES (?, ?, ?)
|
VALUES (?, ?, ?)
|
||||||
@@ -2917,7 +2917,7 @@ async fn apply_group_changes(
|
|||||||
// highest `add_timestamp` to disambiguate.
|
// highest `add_timestamp` to disambiguate.
|
||||||
// The result of the error is that info message
|
// The result of the error is that info message
|
||||||
// may contain display name of the wrong contact.
|
// may contain display name of the wrong contact.
|
||||||
let fingerprint = key.dc_fingerprint().hex();
|
let fingerprint = key.public_key.dc_fingerprint().hex();
|
||||||
if let Some(contact_id) =
|
if let Some(contact_id) =
|
||||||
lookup_key_contact_by_fingerprint(context, &fingerprint).await?
|
lookup_key_contact_by_fingerprint(context, &fingerprint).await?
|
||||||
{
|
{
|
||||||
@@ -3659,10 +3659,28 @@ async fn mark_recipients_as_verified(
|
|||||||
to_ids: &[Option<ContactId>],
|
to_ids: &[Option<ContactId>],
|
||||||
mimeparser: &MimeMessage,
|
mimeparser: &MimeMessage,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
|
let verifier_id = Some(from_id).filter(|&id| id != ContactId::SELF);
|
||||||
|
for gossiped_key in mimeparser
|
||||||
|
.gossiped_keys
|
||||||
|
.values()
|
||||||
|
.filter(|gossiped_key| gossiped_key.verified)
|
||||||
|
{
|
||||||
|
let fingerprint = gossiped_key.public_key.dc_fingerprint().hex();
|
||||||
|
let Some(to_id) = lookup_key_contact_by_fingerprint(context, &fingerprint).await? else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
if to_id == ContactId::SELF || to_id == from_id {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
mark_contact_id_as_verified(context, to_id, verifier_id).await?;
|
||||||
|
ChatId::set_protection_for_contact(context, to_id, mimeparser.timestamp_sent).await?;
|
||||||
|
}
|
||||||
|
|
||||||
if mimeparser.get_header(HeaderDef::ChatVerified).is_none() {
|
if mimeparser.get_header(HeaderDef::ChatVerified).is_none() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
let verifier_id = Some(from_id).filter(|&id| id != ContactId::SELF);
|
|
||||||
for to_id in to_ids.iter().filter_map(|&x| x) {
|
for to_id in to_ids.iter().filter_map(|&x| x) {
|
||||||
if to_id == ContactId::SELF || to_id == from_id {
|
if to_id == ContactId::SELF || to_id == from_id {
|
||||||
continue;
|
continue;
|
||||||
@@ -3755,7 +3773,7 @@ async fn add_or_lookup_contacts_by_address_list(
|
|||||||
async fn add_or_lookup_key_contacts(
|
async fn add_or_lookup_key_contacts(
|
||||||
context: &Context,
|
context: &Context,
|
||||||
address_list: &[SingleInfo],
|
address_list: &[SingleInfo],
|
||||||
gossiped_keys: &HashMap<String, SignedPublicKey>,
|
gossiped_keys: &BTreeMap<String, GossipedKey>,
|
||||||
fingerprints: &[Fingerprint],
|
fingerprints: &[Fingerprint],
|
||||||
origin: Origin,
|
origin: Origin,
|
||||||
) -> Result<Vec<Option<ContactId>>> {
|
) -> Result<Vec<Option<ContactId>>> {
|
||||||
@@ -3771,7 +3789,7 @@ async fn add_or_lookup_key_contacts(
|
|||||||
// Iterator has not ran out of fingerprints yet.
|
// Iterator has not ran out of fingerprints yet.
|
||||||
fp.hex()
|
fp.hex()
|
||||||
} else if let Some(key) = gossiped_keys.get(addr) {
|
} else if let Some(key) = gossiped_keys.get(addr) {
|
||||||
key.dc_fingerprint().hex()
|
key.public_key.dc_fingerprint().hex()
|
||||||
} else if context.is_self_addr(addr).await? {
|
} else if context.is_self_addr(addr).await? {
|
||||||
contact_ids.push(Some(ContactId::SELF));
|
contact_ids.push(Some(ContactId::SELF));
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -272,7 +272,9 @@ pub(crate) async fn handle_securejoin_handshake(
|
|||||||
let mut self_found = false;
|
let mut self_found = false;
|
||||||
let self_fingerprint = load_self_public_key(context).await?.dc_fingerprint();
|
let self_fingerprint = load_self_public_key(context).await?.dc_fingerprint();
|
||||||
for (addr, key) in &mime_message.gossiped_keys {
|
for (addr, key) in &mime_message.gossiped_keys {
|
||||||
if key.dc_fingerprint() == self_fingerprint && context.is_self_addr(addr).await? {
|
if key.public_key.dc_fingerprint() == self_fingerprint
|
||||||
|
&& context.is_self_addr(addr).await?
|
||||||
|
{
|
||||||
self_found = true;
|
self_found = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -542,7 +544,7 @@ pub(crate) async fn observe_securejoin_on_other_device(
|
|||||||
return Ok(HandshakeMessage::Ignore);
|
return Ok(HandshakeMessage::Ignore);
|
||||||
};
|
};
|
||||||
|
|
||||||
if key.dc_fingerprint() != contact_fingerprint {
|
if key.public_key.dc_fingerprint() != contact_fingerprint {
|
||||||
// Fingerprint does not match, ignore.
|
// Fingerprint does not match, ignore.
|
||||||
warn!(context, "Fingerprint does not match.");
|
warn!(context, "Fingerprint does not match.");
|
||||||
return Ok(HandshakeMessage::Ignore);
|
return Ok(HandshakeMessage::Ignore);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ use crate::chat::{CantSendReason, remove_contact_from_chat};
|
|||||||
use crate::chatlist::Chatlist;
|
use crate::chatlist::Chatlist;
|
||||||
use crate::constants::Chattype;
|
use crate::constants::Chattype;
|
||||||
use crate::key::self_fingerprint;
|
use crate::key::self_fingerprint;
|
||||||
|
use crate::mimeparser::GossipedKey;
|
||||||
use crate::receive_imf::receive_imf;
|
use crate::receive_imf::receive_imf;
|
||||||
use crate::stock_str::{self, messages_e2e_encrypted};
|
use crate::stock_str::{self, messages_e2e_encrypted};
|
||||||
use crate::test_utils::{
|
use crate::test_utils::{
|
||||||
@@ -185,7 +186,10 @@ async fn test_setup_contact_ex(case: SetupContactCase) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if case == SetupContactCase::WrongAliceGossip {
|
if case == SetupContactCase::WrongAliceGossip {
|
||||||
let wrong_pubkey = load_self_public_key(&bob).await.unwrap();
|
let wrong_pubkey = GossipedKey {
|
||||||
|
public_key: load_self_public_key(&bob).await.unwrap(),
|
||||||
|
verified: false,
|
||||||
|
};
|
||||||
let alice_pubkey = msg
|
let alice_pubkey = msg
|
||||||
.gossiped_keys
|
.gossiped_keys
|
||||||
.insert(alice_addr.to_string(), wrong_pubkey)
|
.insert(alice_addr.to_string(), wrong_pubkey)
|
||||||
|
|||||||
Reference in New Issue
Block a user