mirror of
https://github.com/chatmail/core.git
synced 2026-10-03 11:40:38 +03:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a1675f46bc | ||
|
|
36384f977f | ||
|
|
7fcc612e1e | ||
|
|
8ab98019a7 |
@@ -27,55 +27,6 @@ use deltachat::sql;
|
||||
use deltachat::tools::*;
|
||||
use tokio::fs;
|
||||
|
||||
/// Reset database tables.
|
||||
/// Argument is a bitmask, executing single or multiple actions in one call.
|
||||
/// e.g. bitmask 7 triggers actions defined with bits 1, 2 and 4.
|
||||
async fn reset_tables(context: &Context, bits: i32) {
|
||||
println!("Resetting tables ({bits})...");
|
||||
if 0 != bits & 4 {
|
||||
context
|
||||
.sql()
|
||||
.execute("DELETE FROM keypairs;", ())
|
||||
.await
|
||||
.unwrap();
|
||||
println!("(4) Private keypairs reset.");
|
||||
}
|
||||
if 0 != bits & 8 {
|
||||
context
|
||||
.sql()
|
||||
.execute("DELETE FROM contacts WHERE id>9;", ())
|
||||
.await
|
||||
.unwrap();
|
||||
context
|
||||
.sql()
|
||||
.execute("DELETE FROM chats WHERE id>9;", ())
|
||||
.await
|
||||
.unwrap();
|
||||
context
|
||||
.sql()
|
||||
.execute("DELETE FROM chats_contacts;", ())
|
||||
.await
|
||||
.unwrap();
|
||||
context
|
||||
.sql()
|
||||
.execute("DELETE FROM msgs WHERE id>9;", ())
|
||||
.await
|
||||
.unwrap();
|
||||
context
|
||||
.sql()
|
||||
.execute(
|
||||
"DELETE FROM config WHERE keyname LIKE 'imap.%' OR keyname LIKE 'configured%';",
|
||||
(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
context.sql().config_cache().write().await.clear();
|
||||
println!("(8) Rest but server config reset.");
|
||||
}
|
||||
|
||||
context.emit_msgs_changed_without_ids();
|
||||
}
|
||||
|
||||
async fn poke_eml_file(context: &Context, filename: &Path) -> Result<()> {
|
||||
let data = read_file(context, filename).await?;
|
||||
|
||||
@@ -304,7 +255,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
export-keys\n\
|
||||
import-keys <key-file>\n\
|
||||
poke [<eml-file>|<folder>|<addr> <key-file>]\n\
|
||||
reset <flags>\n\
|
||||
stop\n\
|
||||
============================================="
|
||||
),
|
||||
@@ -444,15 +394,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
"poke" => {
|
||||
ensure!(poke_spec(&context, Some(arg1)).await, "Poke failed");
|
||||
}
|
||||
"reset" => {
|
||||
ensure!(
|
||||
!arg1.is_empty(),
|
||||
"Argument <bits> missing: 4=private keys, 8=rest but server config"
|
||||
);
|
||||
let bits: i32 = arg1.parse()?;
|
||||
ensure!(bits < 16, "<bits> must be lower than 16.");
|
||||
reset_tables(&context, bits).await;
|
||||
}
|
||||
"stop" => {
|
||||
context.stop_ongoing().await;
|
||||
}
|
||||
|
||||
@@ -147,7 +147,7 @@ impl Completer for DcHelper {
|
||||
}
|
||||
}
|
||||
|
||||
const IMEX_COMMANDS: [&str; 10] = [
|
||||
const IMEX_COMMANDS: [&str; 9] = [
|
||||
"has-backup",
|
||||
"export-backup",
|
||||
"import-backup",
|
||||
@@ -156,7 +156,6 @@ const IMEX_COMMANDS: [&str; 10] = [
|
||||
"export-keys",
|
||||
"import-keys",
|
||||
"poke",
|
||||
"reset",
|
||||
"stop",
|
||||
];
|
||||
|
||||
|
||||
+1
-4
@@ -124,14 +124,11 @@ pub trait DcKey: Serialize + Deserializable + Clone {
|
||||
|
||||
/// Converts secret key to public key.
|
||||
pub(crate) fn secret_key_to_public_key(
|
||||
context: &Context,
|
||||
mut signed_secret_key: SignedSecretKey,
|
||||
timestamp: u32,
|
||||
addr: &str,
|
||||
relay_addrs: &str,
|
||||
) -> Result<SignedPublicKey> {
|
||||
info!(context, "Converting secret key to public key.");
|
||||
|
||||
// Make sure timestamp of created signatures
|
||||
// is not in the past compared to the primary key timestamp.
|
||||
let timestamp = std::cmp::max(
|
||||
@@ -304,7 +301,7 @@ pub(crate) async fn load_self_public_key_opt(context: &Context) -> Result<Option
|
||||
let addr = context.get_primary_self_addr().await?;
|
||||
let all_addrs = context.get_self_addrs().await?.join(",");
|
||||
let signed_public_key =
|
||||
secret_key_to_public_key(context, signed_secret_key, timestamp, &addr, &all_addrs)?;
|
||||
secret_key_to_public_key(signed_secret_key, timestamp, &addr, &all_addrs)?;
|
||||
*lock = Some(signed_public_key.clone());
|
||||
|
||||
Ok(Some(signed_public_key))
|
||||
|
||||
+5
-2
@@ -331,9 +331,10 @@ pub(crate) fn render_queued_mail(
|
||||
outer_headers.extend(b"MIME-Version: 1.0\r\n");
|
||||
|
||||
if should_attach_pubkey {
|
||||
let public_key = crate::pgp::minimize_autocrypt_certificate(public_key);
|
||||
let aheader = Aheader {
|
||||
addr: from_addr,
|
||||
public_key: public_key.clone(),
|
||||
public_key,
|
||||
prefer_encrypt: EncryptPreference::Mutual,
|
||||
};
|
||||
let autocrypt_header = mail_builder::headers::raw::Raw::new(aheader.to_string());
|
||||
@@ -1472,9 +1473,11 @@ impl MimeFactory {
|
||||
continue;
|
||||
}
|
||||
|
||||
let public_key = crate::pgp::minimize_autocrypt_certificate(key);
|
||||
|
||||
let header = Aheader {
|
||||
addr: addr.clone(),
|
||||
public_key: key.clone(),
|
||||
public_key,
|
||||
// Autocrypt 1.1.0 specification says that
|
||||
// `prefer-encrypt` attribute SHOULD NOT be included.
|
||||
prefer_encrypt: EncryptPreference::NoPreference,
|
||||
|
||||
@@ -341,7 +341,6 @@ async fn test_mdn_sent_to_all_relays() -> Result<()> {
|
||||
// Bob's key gets a second relay address and Alice merges the newer key.
|
||||
let bob_secret_key = load_self_secret_key(bob).await?;
|
||||
let bob_public_key = secret_key_to_public_key(
|
||||
bob,
|
||||
bob_secret_key,
|
||||
u32::try_from(time())? + 100,
|
||||
"bob@example.net",
|
||||
|
||||
+266
-10
@@ -1,6 +1,8 @@
|
||||
//! OpenPGP helper module using [rPGP facilities](https://github.com/rpgp/rpgp).
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::cmp::Ordering;
|
||||
use std::collections::btree_map::Entry as BTreeMapEntry;
|
||||
use std::collections::{BTreeMap, HashMap, HashSet};
|
||||
use std::io::Cursor;
|
||||
|
||||
use anyhow::{Context as _, Result, ensure};
|
||||
@@ -14,7 +16,7 @@ use pgp::crypto::aead::{AeadAlgorithm, ChunkSize};
|
||||
use pgp::crypto::ecc_curve::ECCCurve;
|
||||
use pgp::crypto::hash::HashAlgorithm;
|
||||
use pgp::crypto::sym::SymmetricKeyAlgorithm;
|
||||
use pgp::packet::{Signature, Subpacket, SubpacketData};
|
||||
use pgp::packet::{Signature, SignatureType, Subpacket, SubpacketData};
|
||||
use pgp::types::{
|
||||
CompressionAlgorithm, Imprint, KeyDetails, KeyVersion, Password, SignedUser, SigningKey as _,
|
||||
StringToKey,
|
||||
@@ -83,13 +85,62 @@ pub(crate) fn create_keypair(addr: EmailAddress) -> Result<SignedSecretKey> {
|
||||
|
||||
/// Selects a subkey of the public key to use for encryption.
|
||||
///
|
||||
/// Returns `None` if the public key cannot be used for encryption.
|
||||
/// The key is selected according to
|
||||
/// <https://www.ietf.org/archive/id/draft-autocrypt-openpgp-v2-cert-03.html#section-4.3-4>.
|
||||
/// If multiple keys are available, the one that will expire sooner is selected.
|
||||
///
|
||||
/// TODO: take key flags and expiration dates into account
|
||||
fn select_pk_for_encryption(key: &SignedPublicKey) -> Option<&SignedPublicSubKey> {
|
||||
/// Returns `None` if the public key cannot be used for encryption.
|
||||
fn select_pk_for_encryption(now: u32, key: &SignedPublicKey) -> Option<&SignedPublicSubKey> {
|
||||
key.public_subkeys
|
||||
.iter()
|
||||
.find(|subkey| subkey.algorithm().can_encrypt())
|
||||
.filter(|subkey| subkey.algorithm().can_encrypt())
|
||||
.filter_map(|subkey| {
|
||||
let signature = subkey.signatures.first()?;
|
||||
|
||||
let key_flags = signature.key_flags();
|
||||
if !key_flags.encrypt_comms() {
|
||||
return None;
|
||||
}
|
||||
|
||||
if let Some(expiration_duration) = signature
|
||||
.key_expiration_time()
|
||||
.filter(|duration| duration.as_secs() != 0)
|
||||
&& now
|
||||
> subkey
|
||||
.created_at()
|
||||
.as_secs()
|
||||
.saturating_add(expiration_duration.as_secs())
|
||||
{
|
||||
// Key is expired.
|
||||
return None;
|
||||
}
|
||||
Some((subkey, signature))
|
||||
})
|
||||
.min_by(|(subkey1, signature1), (subkey2, signature2)| {
|
||||
match (
|
||||
signature1
|
||||
.key_expiration_time()
|
||||
.filter(|duration| duration.as_secs() != 0),
|
||||
signature2
|
||||
.key_expiration_time()
|
||||
.filter(|duration| duration.as_secs() != 0),
|
||||
) {
|
||||
(None, None) => Ordering::Equal,
|
||||
(None, Some(_)) => Ordering::Greater,
|
||||
(Some(_), None) => Ordering::Less,
|
||||
(Some(expiration1), Some(expiration2)) => (subkey1
|
||||
.created_at()
|
||||
.as_secs()
|
||||
.saturating_add(expiration1.as_secs()))
|
||||
.cmp(
|
||||
&(subkey2
|
||||
.created_at()
|
||||
.as_secs()
|
||||
.saturating_add(expiration2.as_secs())),
|
||||
),
|
||||
}
|
||||
})
|
||||
.map(|(subkey, _signature)| subkey)
|
||||
}
|
||||
|
||||
/// Version of SEIPD packet to use.
|
||||
@@ -151,10 +202,11 @@ pub fn pk_encrypt(
|
||||
) -> Result<String> {
|
||||
tokio::task::block_in_place(|| {
|
||||
let mut rng = thread_rng();
|
||||
let now = pgp::types::Timestamp::now();
|
||||
|
||||
let pkeys = public_keys_for_encryption
|
||||
.iter()
|
||||
.filter_map(select_pk_for_encryption);
|
||||
.filter_map(|key| select_pk_for_encryption(now.as_secs(), key));
|
||||
|
||||
let msg = MessageBuilder::from_bytes("", plain);
|
||||
let encoded_msg = match seipd_version {
|
||||
@@ -310,6 +362,161 @@ pub fn symm_encrypt_message(
|
||||
})
|
||||
}
|
||||
|
||||
/// Minimizes the signatures of a subkey.
|
||||
///
|
||||
/// Keeps at most one subkey binding signature
|
||||
/// and at most one revocation signature,
|
||||
/// preferring the newest signatures.
|
||||
///
|
||||
/// Subkey binding signature is kept
|
||||
/// even if the revocation signature exists
|
||||
/// because according to
|
||||
/// <https://www.rfc-editor.org/rfc/rfc9580.html#name-openpgp-version-6-certifica>
|
||||
/// "Every subkey MUST have at least one Subkey Binding signature."
|
||||
/// Distributing subkey with only a revocation signature
|
||||
/// is not allowed according to the standard,
|
||||
/// so we keep a subkey binding signature next to it
|
||||
/// for interoperability.
|
||||
///
|
||||
/// This function does not check if the signatures are valid.
|
||||
/// Such properties should be validated when importing OpenPGP certificates.
|
||||
fn minimize_subpacket_signatures(signatures: Vec<Signature>) -> Vec<Signature> {
|
||||
let mut newest_revocation_signature: Option<Signature> = None;
|
||||
let mut newest_binding_signature: Option<Signature> = None;
|
||||
for signature in signatures {
|
||||
let Some(config) = signature.config() else {
|
||||
// Skip unknown signatures.
|
||||
continue;
|
||||
};
|
||||
match config.typ {
|
||||
SignatureType::SubkeyBinding => {
|
||||
if newest_binding_signature
|
||||
.as_ref()
|
||||
.is_none_or(|s| s.created() < signature.created())
|
||||
{
|
||||
newest_binding_signature = Some(signature)
|
||||
}
|
||||
}
|
||||
SignatureType::SubkeyRevocation => {
|
||||
if newest_revocation_signature
|
||||
.as_ref()
|
||||
.is_none_or(|s| s.created() < signature.created())
|
||||
{
|
||||
newest_revocation_signature = Some(signature)
|
||||
}
|
||||
}
|
||||
_ => continue,
|
||||
}
|
||||
}
|
||||
newest_revocation_signature
|
||||
.into_iter()
|
||||
.chain(newest_binding_signature)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Minimizes OpenPGP certificate for Autocrypt and Autocrypt-Gossip headers.
|
||||
pub fn minimize_autocrypt_certificate(certificate: &SignedPublicKey) -> SignedPublicKey {
|
||||
let primary_key = certificate.primary_key.clone();
|
||||
let details = certificate.details.clone();
|
||||
|
||||
// Select the newest non-expiring subkey and the newest expiring subkey.
|
||||
let fallback_subkey = certificate
|
||||
.public_subkeys
|
||||
.iter()
|
||||
.filter(|subkey| {
|
||||
subkey
|
||||
.signatures
|
||||
.iter()
|
||||
.find(|signature| {
|
||||
signature
|
||||
.config()
|
||||
.is_some_and(|config| config.typ == SignatureType::SubkeyBinding)
|
||||
})
|
||||
.is_some_and(|signature| signature.key_expiration_time().is_none())
|
||||
})
|
||||
.max_by_key(|subkey| {
|
||||
subkey
|
||||
.signatures
|
||||
.iter()
|
||||
.find(|signature| {
|
||||
signature
|
||||
.config()
|
||||
.is_some_and(|config| config.typ == SignatureType::SubkeyBinding)
|
||||
})
|
||||
.map(|signature| signature.created().unwrap_or(subkey.created_at()))
|
||||
});
|
||||
let rotating_subkey = certificate
|
||||
.public_subkeys
|
||||
.iter()
|
||||
.filter(|subkey| {
|
||||
subkey
|
||||
.signatures
|
||||
.iter()
|
||||
.find(|signature| {
|
||||
signature
|
||||
.config()
|
||||
.is_some_and(|config| config.typ == SignatureType::SubkeyBinding)
|
||||
})
|
||||
.is_some_and(|signature| signature.key_expiration_time().is_some())
|
||||
})
|
||||
.max_by_key(|subkey| {
|
||||
subkey
|
||||
.signatures
|
||||
.iter()
|
||||
.find(|signature| {
|
||||
signature
|
||||
.config()
|
||||
.is_some_and(|config| config.typ == SignatureType::SubkeyBinding)
|
||||
})
|
||||
.map(|signature| signature.created().unwrap_or(subkey.created_at()))
|
||||
});
|
||||
let public_subkeys: Vec<_> = fallback_subkey
|
||||
.into_iter()
|
||||
.chain(rotating_subkey)
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
// We do not want to ever gossip more than two subkeys
|
||||
// to save the traffic.
|
||||
debug_assert!(public_subkeys.len() <= 2);
|
||||
|
||||
SignedPublicKey {
|
||||
primary_key,
|
||||
details,
|
||||
public_subkeys,
|
||||
}
|
||||
}
|
||||
|
||||
/// Merges two OpenPGP subkeys.
|
||||
fn merge_openpgp_subkey(old_subkey: &mut SignedPublicSubKey, new_subkey: SignedPublicSubKey) {
|
||||
debug_assert_eq!(old_subkey.fingerprint(), new_subkey.fingerprint());
|
||||
old_subkey.signatures = minimize_subpacket_signatures(
|
||||
std::mem::take(&mut old_subkey.signatures)
|
||||
.into_iter()
|
||||
.chain(new_subkey.signatures)
|
||||
.collect(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Merges OpenPGP subkey vectors.
|
||||
pub fn merge_openpgp_subkeys(
|
||||
subkeys: impl IntoIterator<Item = SignedPublicSubKey>,
|
||||
) -> Result<Vec<SignedPublicSubKey>> {
|
||||
let mut merged_subkeys: BTreeMap<_, SignedPublicSubKey> = BTreeMap::new();
|
||||
for subkey in subkeys {
|
||||
let imprint = subkey.imprint::<Sha256>()?;
|
||||
match merged_subkeys.entry(imprint) {
|
||||
BTreeMapEntry::Vacant(entry) => {
|
||||
entry.insert(subkey);
|
||||
}
|
||||
BTreeMapEntry::Occupied(entry) => {
|
||||
merge_openpgp_subkey(entry.into_mut(), subkey);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(merged_subkeys.into_values().collect())
|
||||
}
|
||||
|
||||
/// Merges and minimizes OpenPGP certificates.
|
||||
///
|
||||
/// Keeps at most one direct key signature and
|
||||
@@ -346,7 +553,7 @@ pub fn merge_openpgp_certificates(
|
||||
let SignedPublicKey {
|
||||
primary_key: new_primary_key,
|
||||
details: new_details,
|
||||
public_subkeys: _new_public_subkeys,
|
||||
public_subkeys: new_public_subkeys,
|
||||
} = new_certificate;
|
||||
|
||||
// Public keys may be serialized differently, e.g. using old and new packet type,
|
||||
@@ -422,7 +629,55 @@ pub fn merge_openpgp_certificates(
|
||||
});
|
||||
let users: Vec<SignedUser> = best_user.into_iter().collect();
|
||||
|
||||
let public_subkeys = old_public_subkeys;
|
||||
let (fallback_subkeys, mut rotating_subkeys): (Vec<_>, Vec<_>) =
|
||||
merge_openpgp_subkeys(old_public_subkeys.into_iter().chain(new_public_subkeys))?
|
||||
.into_iter()
|
||||
.filter_map(|subkey| {
|
||||
// Select the newest subkey binding signature.
|
||||
//
|
||||
// There is at most one subkey binding signature at this point
|
||||
// because older subkey binding signatures are removed during merging.
|
||||
let signature = subkey.signatures.iter().find(|signature| {
|
||||
signature
|
||||
.config()
|
||||
.is_some_and(|config| config.typ == SignatureType::SubkeyBinding)
|
||||
})?;
|
||||
|
||||
let created_at_secs = signature.created().unwrap_or(subkey.created_at()).as_secs();
|
||||
let expires_at_secs: Option<u32> = signature
|
||||
.key_expiration_time()
|
||||
.map(|duration| duration.as_secs())
|
||||
.filter(|duration_secs| *duration_secs != 0)
|
||||
.map(|duration_secs| {
|
||||
subkey.created_at().as_secs().saturating_add(duration_secs)
|
||||
});
|
||||
|
||||
Some((subkey, created_at_secs, expires_at_secs))
|
||||
})
|
||||
.partition(|(_subkey, _created_at_secs, expires_at_secs)| expires_at_secs.is_none());
|
||||
let fallback_subkey: Option<SignedPublicSubKey> = fallback_subkeys
|
||||
.into_iter()
|
||||
.max_by_key(|(_subkey, created_at_secs, _)| *created_at_secs)
|
||||
.map(|(subkey, _, _)| subkey);
|
||||
|
||||
rotating_subkeys
|
||||
.sort_by_key(|(_subkey, created_at_secs, _)| std::cmp::Reverse(*created_at_secs));
|
||||
|
||||
// Put the fallback subkey first so it is gossiped first.
|
||||
//
|
||||
// We want to always gossip non-expiring key first
|
||||
// for older versions that always encrypted to the first subkey.
|
||||
//
|
||||
// Keep 10 newest rotating subkeys to avoid storing indefinitely growing number of subkeys locally.
|
||||
let public_subkeys = fallback_subkey
|
||||
.into_iter()
|
||||
.chain(
|
||||
rotating_subkeys
|
||||
.into_iter()
|
||||
.take(10)
|
||||
.map(|(subkey, _, _)| subkey),
|
||||
)
|
||||
.collect();
|
||||
|
||||
Ok(SignedPublicKey {
|
||||
primary_key: old_primary_key,
|
||||
@@ -485,7 +740,8 @@ pub(crate) fn relay_addrs(public_key: &SignedPublicKey, addr: &str) -> Vec<Strin
|
||||
|
||||
/// Returns true if the key can be encrypted to, i.e. has an encryption subkey.
|
||||
pub(crate) fn pubkey_can_encrypt(public_key: &SignedPublicKey) -> bool {
|
||||
select_pk_for_encryption(public_key).is_some()
|
||||
let now = pgp::types::Timestamp::now();
|
||||
select_pk_for_encryption(now.as_secs(), public_key).is_some()
|
||||
}
|
||||
|
||||
/// Returns true if public key advertises SEIPDv2 feature.
|
||||
|
||||
Reference in New Issue
Block a user