diff --git a/src/key.rs b/src/key.rs index b9e88f8be..38d7bf2a2 100644 --- a/src/key.rs +++ b/src/key.rs @@ -355,7 +355,7 @@ pub async fn store_self_keypair( } /// A key fingerprint -#[derive(Debug, Clone, Eq, PartialEq, Hash)] +#[derive(Clone, Eq, PartialEq, Hash)] pub struct Fingerprint(Vec); impl Fingerprint { @@ -375,6 +375,14 @@ impl Fingerprint { } } +impl fmt::Debug for Fingerprint { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Fingerprint") + .field("hex", &self.hex()) + .finish() + } +} + /// Make a human-readable fingerprint. impl fmt::Display for Fingerprint { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { diff --git a/src/securejoin.rs b/src/securejoin.rs index d907a0680..e408cb4c9 100644 --- a/src/securejoin.rs +++ b/src/securejoin.rs @@ -1088,3 +1088,222 @@ fn encrypted_and_signed( false } } + +#[cfg(test)] +mod tests { + use super::*; + + use crate::peerstate::Peerstate; + use crate::test_utils::TestContext; + + #[async_std::test] + async fn test_setup_contact() { + let alice = TestContext::new_alice().await; + let bob = TestContext::new_bob().await; + + // Generate QR-code, ChatId(0) indicates setup-contact + let qr = dc_get_securejoin_qr(&alice.ctx, ChatId::new(0)) + .await + .unwrap(); + + // Bob scans QR-code, sends vc-request + let bob_chatid = dc_join_securejoin(&bob.ctx, &qr).await; + + let sent = bob.pop_sent_msg().await; + assert_eq!(sent.id(), bob_chatid); + assert_eq!(sent.recipient(), "alice@example.com".parse().unwrap()); + let msg = alice.parse_msg(&sent).await; + assert!(!msg.was_encrypted()); + assert_eq!(msg.get(HeaderDef::SecureJoin).unwrap(), "vc-request"); + assert!(msg.get(HeaderDef::SecureJoinInvitenumber).is_some()); + + // Alice receives vc-request, sends vc-auth-required + alice.recv_msg(&sent).await; + + let sent = alice.pop_sent_msg().await; + let msg = bob.parse_msg(&sent).await; + assert!(msg.was_encrypted()); + assert_eq!(msg.get(HeaderDef::SecureJoin).unwrap(), "vc-auth-required"); + + // Bob receives vc-auth-required, sends vc-request-with-auth + bob.recv_msg(&sent).await; + + let sent = bob.pop_sent_msg().await; + let msg = alice.parse_msg(&sent).await; + assert!(msg.was_encrypted()); + assert_eq!( + msg.get(HeaderDef::SecureJoin).unwrap(), + "vc-request-with-auth" + ); + assert!(msg.get(HeaderDef::SecureJoinAuth).is_some()); + let bob_fp = SignedPublicKey::load_self(&bob.ctx) + .await + .unwrap() + .fingerprint(); + assert_eq!( + *msg.get(HeaderDef::SecureJoinFingerprint).unwrap(), + bob_fp.hex() + ); + + // Alice should not yet have Bob verified + let contact_bob_id = + Contact::lookup_id_by_addr(&alice.ctx, "bob@example.net", Origin::Unknown).await; + let contact_bob = Contact::load_from_db(&alice.ctx, contact_bob_id) + .await + .unwrap(); + assert_eq!( + contact_bob.is_verified(&alice.ctx).await, + VerifiedStatus::Unverified + ); + + // Alice receives vc-request-with-auth, sends vc-contact-confirm + alice.recv_msg(&sent).await; + assert_eq!( + contact_bob.is_verified(&alice.ctx).await, + VerifiedStatus::BidirectVerified + ); + + let sent = alice.pop_sent_msg().await; + let msg = bob.parse_msg(&sent).await; + assert!(msg.was_encrypted()); + assert_eq!( + msg.get(HeaderDef::SecureJoin).unwrap(), + "vc-contact-confirm" + ); + + // Bob should not yet have Alice verified + let contact_alice_id = + Contact::lookup_id_by_addr(&bob.ctx, "alice@example.com", Origin::Unknown).await; + let contact_alice = Contact::load_from_db(&bob.ctx, contact_alice_id) + .await + .unwrap(); + assert_eq!( + contact_bob.is_verified(&bob.ctx).await, + VerifiedStatus::Unverified + ); + + // Bob receives vc-contact-confirm, sends vc-contact-confirm-received + bob.recv_msg(&sent).await; + assert_eq!( + contact_alice.is_verified(&bob.ctx).await, + VerifiedStatus::BidirectVerified + ); + + let sent = bob.pop_sent_msg().await; + let msg = alice.parse_msg(&sent).await; + assert!(msg.was_encrypted()); + assert_eq!( + msg.get(HeaderDef::SecureJoin).unwrap(), + "vc-contact-confirm-received" + ); + } + + #[async_std::test] + async fn test_setup_contact_bob_knows_alice() { + let alice = TestContext::new_alice().await; + let bob = TestContext::new_bob().await; + + // Ensure Bob knows Alice_FP + let alice_pubkey = SignedPublicKey::load_self(&alice.ctx).await.unwrap(); + let peerstate = Peerstate { + context: &bob.ctx, + addr: "alice@example.com".into(), + last_seen: 10, + last_seen_autocrypt: 10, + prefer_encrypt: EncryptPreference::Mutual, + public_key: Some(alice_pubkey.clone()), + public_key_fingerprint: Some(alice_pubkey.fingerprint()), + gossip_key: Some(alice_pubkey.clone()), + gossip_timestamp: 10, + gossip_key_fingerprint: Some(alice_pubkey.fingerprint()), + verified_key: None, + verified_key_fingerprint: None, + to_save: Some(ToSave::All), + fingerprint_changed: false, + }; + peerstate.save_to_db(&bob.ctx.sql, true).await.unwrap(); + + // Generate QR-code, ChatId(0) indicates setup-contact + let qr = dc_get_securejoin_qr(&alice.ctx, ChatId::new(0)) + .await + .unwrap(); + + // Bob scans QR-code, sends vc-request-with-auth, skipping vc-request + dc_join_securejoin(&bob.ctx, &qr).await; + + let sent = bob.pop_sent_msg().await; + let msg = alice.parse_msg(&sent).await; + assert!(msg.was_encrypted()); + assert_eq!( + msg.get(HeaderDef::SecureJoin).unwrap(), + "vc-request-with-auth" + ); + assert!(msg.get(HeaderDef::SecureJoinAuth).is_some()); + let bob_fp = SignedPublicKey::load_self(&bob.ctx) + .await + .unwrap() + .fingerprint(); + assert_eq!( + *msg.get(HeaderDef::SecureJoinFingerprint).unwrap(), + bob_fp.hex() + ); + + // Alice should not yet have Bob verified + let (contact_bob_id, _modified) = Contact::add_or_lookup( + &alice.ctx, + "Bob", + "bob@example.net", + Origin::ManuallyCreated, + ) + .await + .unwrap(); + let contact_bob = Contact::load_from_db(&alice.ctx, contact_bob_id) + .await + .unwrap(); + assert_eq!( + contact_bob.is_verified(&alice.ctx).await, + VerifiedStatus::Unverified + ); + + // Alice receives vc-request-with-auth, sends vc-contact-confirm + alice.recv_msg(&sent).await; + assert_eq!( + contact_bob.is_verified(&alice.ctx).await, + VerifiedStatus::BidirectVerified + ); + + let sent = alice.pop_sent_msg().await; + let msg = bob.parse_msg(&sent).await; + assert!(msg.was_encrypted()); + assert_eq!( + msg.get(HeaderDef::SecureJoin).unwrap(), + "vc-contact-confirm" + ); + + // Bob should not yet have Alice verified + let contact_alice_id = + Contact::lookup_id_by_addr(&bob.ctx, "alice@example.com", Origin::Unknown).await; + let contact_alice = Contact::load_from_db(&bob.ctx, contact_alice_id) + .await + .unwrap(); + assert_eq!( + contact_bob.is_verified(&bob.ctx).await, + VerifiedStatus::Unverified + ); + + // Bob receives vc-contact-confirm, sends vc-contact-confirm-received + bob.recv_msg(&sent).await; + assert_eq!( + contact_alice.is_verified(&bob.ctx).await, + VerifiedStatus::BidirectVerified + ); + + let sent = bob.pop_sent_msg().await; + let msg = alice.parse_msg(&sent).await; + assert!(msg.was_encrypted()); + assert_eq!( + msg.get(HeaderDef::SecureJoin).unwrap(), + "vc-contact-confirm-received" + ); + } +} diff --git a/src/test_utils.rs b/src/test_utils.rs index cccd7548a..be2b91b77 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -2,12 +2,21 @@ //! //! This module is only compiled for test runs. +use std::cell::RefCell; +use std::str::FromStr; + +use async_std::path::PathBuf; use tempfile::{tempdir, TempDir}; +use crate::chat::ChatId; use crate::config::Config; use crate::context::Context; +use crate::dc_receive_imf::dc_receive_imf; use crate::dc_tools::EmailAddress; +use crate::job::Action; use crate::key::{self, DcKey}; +use crate::mimeparser::MimeMessage; +use crate::param::{Param, Params}; /// A Context and temporary directory. /// @@ -16,6 +25,8 @@ use crate::key::{self, DcKey}; pub(crate) struct TestContext { pub ctx: Context, pub dir: TempDir, + /// Counter for fake IMAP UIDs in [recv_msg], for private use in that function only. + recv_idx: RefCell, } impl TestContext { @@ -35,7 +46,11 @@ impl TestContext { let ctx = Context::new("FakeOS".into(), dbfile.into(), id) .await .unwrap(); - Self { ctx, dir } + Self { + ctx, + dir, + recv_idx: RefCell::new(0), + } } /// Create a new configured [TestContext]. @@ -48,6 +63,19 @@ impl TestContext { t } + /// Create a new configured [TestContext]. + /// + /// This is a shortcut which configures bob@example.net with a fixed key. + pub async fn new_bob() -> Self { + let t = Self::new().await; + let keypair = bob_keypair(); + t.configure_addr(&keypair.addr.to_string()).await; + key::store_self_keypair(&t.ctx, &keypair, key::KeyPairUse::Default) + .await + .expect("Failed to save Bob's key"); + t + } + /// Configure with alice@example.com. /// /// The context will be fake-configured as the alice user, with a pre-generated secret @@ -76,6 +104,112 @@ impl TestContext { .await .unwrap(); } + + /// Retrieve a sent message from the jobs table. + /// + /// This retrieves and removes a message which has been scheduled to send from the jobs + /// table. Messages are returned in the order they have been sent. + /// + /// Panics if there is no message or on any error. + pub async fn pop_sent_msg(&self) -> SentMessage { + let (rowid, foreign_id, raw_params) = self + .ctx + .sql + .query_row( + r#" + SELECT id, foreign_id, param + FROM jobs + WHERE action=? + ORDER BY desired_timestamp; + "#, + paramsv![Action::SendMsgToSmtp], + |row| { + let id: i64 = row.get(0)?; + let foreign_id: i64 = row.get(1)?; + let param: String = row.get(2)?; + Ok((id, foreign_id, param)) + }, + ) + .await + .expect("no sent message found in jobs table"); + let id = ChatId::new(foreign_id as u32); + let params = Params::from_str(&raw_params).unwrap(); + let blob_path = params + .get_blob(Param::File, &self.ctx, false) + .await + .expect("failed to parse blob from param") + .expect("no Param::File found in Params") + .to_abs_path(); + self.ctx + .sql + .execute("DELETE FROM jobs WHERE id=?;", paramsv![rowid]) + .await + .expect("failed to remove job"); + SentMessage { + id, + params, + blob_path, + } + } + + /// Parse a message. + /// + /// Parsing a message does not run the entire receive pipeline, but is not without + /// side-effects either. E.g. if the message includes autocrypt headers the relevant + /// peerstates will be updated. Later receiving the message using [recv_msg] is + /// unlikely to be affected as the peerstate would be processed again in exactly the + /// same way. + pub async fn parse_msg(&self, msg: &SentMessage) -> MimeMessage { + MimeMessage::from_bytes(&self.ctx, msg.payload().as_bytes()) + .await + .unwrap() + } + + /// Receive a message. + /// + /// Receives a message using the `dc_receive_imf()` pipeline. + pub async fn recv_msg(&self, msg: &SentMessage) { + let mut idx = self.recv_idx.borrow_mut(); + *idx += 1; + dc_receive_imf(&self.ctx, msg.payload().as_bytes(), "INBOX", *idx, false) + .await + .unwrap(); + } +} + +/// A raw message as it was scheduled to be sent. +/// +/// This is a raw message, probably in the shape DC was planning to send it but not having +/// passed through a SMTP-IMAP pipeline. +#[derive(Debug, Clone)] +pub struct SentMessage { + id: ChatId, + params: Params, + blob_path: PathBuf, +} + +impl SentMessage { + /// The ChatId the message belonged to. + pub fn id(&self) -> ChatId { + self.id + } + + /// A recipient the message was destined for. + /// + /// If there are multiple recipients this is just a random one, so is not very useful. + pub fn recipient(&self) -> EmailAddress { + let raw = self + .params + .get(Param::Recipients) + .expect("no recipients in params"); + let rcpt = raw.split(' ').next().expect("no recipient found"); + rcpt.parse().expect("failed to parse email address") + } + + /// The raw message payload. + pub fn payload(&self) -> String { + std::fs::read_to_string(&self.blob_path).unwrap() + } } /// Load a pre-generated keypair for alice@example.com from disk.