diff --git a/CHANGELOG.md b/CHANGELOG.md
index 84c5b7cbb..e94d2e788 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,9 @@
## Unreleased
### Changes
+- Implemented "Automatic e-mail address Porting" (AEAP). You can
+ configure a new address in DC now, and when receivers get messages
+ they will automatically recognize your moving to a new address. #3385
- switch from `async-std` to `tokio` as the async runtime #3449
- upgrade to `pgp@0.8.0` #3467
- add IMAP ID extension support #3468
diff --git a/draft/aeap-mvp.md b/draft/aeap-mvp.md
index b081276a2..4cab44a7c 100644
--- a/draft/aeap-mvp.md
+++ b/draft/aeap-mvp.md
@@ -11,26 +11,49 @@ Changes to the UIs
Changes in the core
-------------------
-- DONE: We have one primary self address and any number of secondary self addresses. `is_self_addr()` checks all of them.
+- [x] We have one primary self address and any number of secondary self addresses. `is_self_addr()` checks all of them.
-- DONE: If the user does a reconfigure and changes the email address, the previous address is added as a secondary self address.
+- [x] If the user does a reconfigure and changes the email address, the previous address is added as a secondary self address.
- don't forget to deduplicate secondary self addresses in case the user switches back and forth between addresses).
- The key stays the same.
-- No changes for 1:1 chats, there simply is a new one. (This works since, contrary to group messages, messages sent to a 1:1 chat are not assigned to the group chat but always to the 1:1 chat with the sender. So it's not a problem that the new messages might be put into the old chat if they are a reply to a message there.)
+- [x] No changes for 1:1 chats, there simply is a new one. (This works since, contrary to group messages, messages sent to a 1:1 chat are not assigned to the group chat but always to the 1:1 chat with the sender. So it's not a problem that the new messages might be put into the old chat if they are a reply to a message there.)
-- When sending a message: If any of the secondary self addrs is in the chat's member list, remove it locally (because we just transitioned away from it). We add a log message for this (alternatively, a system message in the chat would be more visible).
+- [ ] When sending a message: If any of the secondary self addrs is in the chat's member list, remove it locally (because we just transitioned away from it). We add a log message for this (alternatively, a system message in the chat would be more visible).
-- When receiving a message: If the key exists, but belongs to another address (we may want to benchmark this)
+- [x] ([#3385](https://github.com/deltachat/deltachat-core-rust/pull/3385)) When receiving a message: If the key exists, but belongs to another address (we may want to benchmark this)
AND there is a `Chat-Version` header\
+ AND the message is signed correctly
+ AND the From address is (also) in the encrypted (and therefore signed) headers [[1]](#myfootnote1)\
AND the message timestamp is newer than the contact's `lastseen` (to prevent changing the address back when messages arrive out of order) (this condition is not that important since we will have eventual consistency even without it):
Replace the contact in _all_ groups, possibly deduplicate the members list, and add a system message to all of these chats.
- Note that we can't simply compare the keys byte-by-byte, since the UID may have changed, or the sender may have rotated the key and signed the new key with the old one.
+[1]: Without this check, an attacker could replay a message from Alice to Bob. Then Bob's device would do an AEAP transition from Alice's to the attacker's address, allowing for easier phishing.
+
+
+More details about this
+Suppose Alice sends a message to Evil (or to a group with both Evil and Bob). Evil then forwards the message to Bob, changing the From and To headers (and if necessary Message-Id) and replacing `addr=alice@example.org;` in the autocrypt header with `addr=evil@example.org;`.
+
+Then Bob's device sees that there is a message which is signed by Alice's key and comes from Evil's address and would do the AEAP transition, i.e. replace Alice with Evil in all groups and show a message "Alice changed their address from alice@example.org to evil@example.org". Disadvantages for Evil are that Bob's message will be shown on Alice's device, possibly creating confusion/suspicion, and that the usual "Setup changed for..." message will be shown the next time Evil sends a message (because Evil doesn't know Alice's private key).
+
+Possible mitigations:
+- if we make the AEAP device message sth. like "Automatically removed alice@example.org and added evil@example.org", then this will create more suspicion, making the phishing harder (we didn't talk about what what the wording should be at all yet).
+- Add something similar to replay protection to our Autocrypt implementation. This could be done e.g. by adding a second `From` header to the protected headers. If it's present, the receiver then requires it to be the same as the outer `From`, and if it's not present, we don't do AEAP --> **That's what we implemented**
+
+Note that usually a mail is signed by a key that has a UID matching the from address.
+
+ That's not mandatory for Autocrypt (and in fact, we just keep the old UID when changing the self address, so with AEAP the UID will actually be different than the from address sometimes)
+
+ https://autocrypt.org/level1.html#openpgp-based-key-data says:
+ > The content of the user id packet is only decorative
+
+
+
### Notes:
- We treat protected and non-protected chats the same
@@ -97,3 +120,8 @@ Other
-----
- The user is responsible that messages to the old address arrive at the new address, for example by configuring the old provider to forward all emails to the new one.
+
+Notes during implementing
+========================
+
+- As far as I understand the code, unencrypted messages are unsigned. So, the transition only works if both sides have the other side's key.
\ No newline at end of file
diff --git a/spec.md b/spec.md
index 54893eff7..e2042d6d7 100644
--- a/spec.md
+++ b/spec.md
@@ -1,6 +1,6 @@
# chat-mail specification
-Version: 0.33.0
+Version: 0.34.0
Status: In-progress
Format: [Semantic Line Breaks](https://sembr.org/)
@@ -474,4 +474,20 @@ as the sending time of the message as indicated by its Date header,
or the time of first receipt if that date is in the future or unavailable.
+# Transitioning to a new e-mail address (AEAP)
+
+When receiving a message:
+- If the key exists, but belongs to another address
+- AND there is a `Chat-Version` header
+- AND the message is signed correctly
+- AND the From address is (also) in the encrypted (and therefore signed) headers
+- AND the message timestamp is newer than the contact's `lastseen`
+ (to prevent changing the address back when messages arrive out of order)
+ (this condition is not that important
+ since we will have eventual consistency even without it):
+
+ Replace the contact in _all_ groups,
+ possibly deduplicate the members list,
+ and add a system message to all of these chats.
+
Copyright © 2017-2021 Delta Chat contributors.
diff --git a/src/config.rs b/src/config.rs
index 4a38463e5..e5f2863d4 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -437,9 +437,8 @@ mod tests {
use std::string::ToString;
use crate::constants;
- use crate::receive_imf::receive_imf;
use crate::test_utils::TestContext;
- use crate::test_utils::TestContextManager;
+
use num_traits::FromPrimitive;
#[test]
@@ -557,68 +556,4 @@ mod tests {
Ok(())
}
-
- #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
- async fn test_change_primary_self_addr() -> Result<()> {
- let mut tcm = TestContextManager::new().await;
- let alice = tcm.alice().await;
- let bob = tcm.bob().await;
-
- // Alice sends a message to Bob
- let alice_bob_chat = alice.create_chat(&bob).await;
- let sent = alice.send_text(alice_bob_chat.id, "Hi").await;
- let bob_msg = bob.recv_msg(&sent).await;
- bob_msg.chat_id.accept(&bob).await?;
- assert_eq!(bob_msg.text.unwrap(), "Hi");
-
- // Alice changes her self address and reconfigures
- // (ensure_secret_key_exists() is called during configure)
- alice
- .set_primary_self_addr("alice@someotherdomain.xyz")
- .await?;
- crate::e2ee::ensure_secret_key_exists(&alice).await?;
-
- assert_eq!(
- alice.get_primary_self_addr().await?,
- "alice@someotherdomain.xyz"
- );
-
- // Bob sends a message to Alice, encrypting to her previous key
- let sent = bob.send_text(bob_msg.chat_id, "hi back").await;
-
- // Alice set up message forwarding so that she still receives
- // the message with her new address
- let alice_msg = alice.recv_msg(&sent).await;
- assert_eq!(alice_msg.text, Some("hi back".to_string()));
- assert_eq!(alice_msg.get_showpadlock(), true);
- assert_eq!(alice_msg.chat_id, alice_bob_chat.id);
-
- // Even if Bob sends a message to Alice without In-Reply-To,
- // it's still assigned to the 1:1 chat with Bob and not to
- // a group (without secondary addresses, an ad-hoc group
- // would be created)
- receive_imf(
- &alice,
- b"From: bob@example.net
-To: alice@example.org
-Chat-Version: 1.0
-Message-ID: <456@example.com>
-
-Message w/out In-Reply-To
-",
- false,
- )
- .await?;
-
- let alice_msg = alice.get_last_msg().await;
-
- assert_eq!(
- alice_msg.text,
- Some("Message w/out In-Reply-To".to_string())
- );
- assert_eq!(alice_msg.get_showpadlock(), false);
- assert_eq!(alice_msg.chat_id, alice_bob_chat.id);
-
- Ok(())
- }
}
diff --git a/src/e2ee.rs b/src/e2ee.rs
index daa183fdb..935876d02 100644
--- a/src/e2ee.rs
+++ b/src/e2ee.rs
@@ -8,11 +8,13 @@ use num_traits::FromPrimitive;
use crate::aheader::{Aheader, EncryptPreference};
use crate::config::Config;
+use crate::contact::addr_cmp;
use crate::context::Context;
use crate::headerdef::HeaderDef;
use crate::headerdef::HeaderDefMap;
use crate::key::{DcKey, Fingerprint, SignedPublicKey, SignedSecretKey};
use crate::keyring::Keyring;
+use crate::log::LogExt;
use crate::peerstate::{Peerstate, PeerstateVerifiedStatus};
use crate::pgp;
@@ -131,6 +133,56 @@ impl EncryptHelper {
}
}
+/// Applies Autocrypt header to Autocrypt peer state and saves it into the database.
+///
+/// If we already know this fingerprint from another contact's peerstate, return that
+/// peerstate in order to make AEAP work, but don't save it into the db yet.
+///
+/// Returns updated peerstate.
+pub(crate) async fn get_autocrypt_peerstate(
+ context: &Context,
+ from: &str,
+ autocrypt_header: Option<&Aheader>,
+ message_time: i64,
+) -> Result