From 66272f7200f9d07efded03ce83091f3f4a20d313 Mon Sep 17 00:00:00 2001 From: mdecimus Date: Wed, 2 Aug 2023 15:48:15 +0200 Subject: [PATCH] v0.3.3 --- CHANGELOG.md | 12 + Cargo.lock | 32 ++- README.md | 2 +- crates/cli/Cargo.toml | 2 +- crates/imap/Cargo.toml | 2 +- crates/install/Cargo.toml | 2 +- crates/jmap/Cargo.toml | 2 +- crates/jmap/src/auth/oauth/mod.rs | 1 + crates/jmap/src/email/crypto.rs | 112 +++++---- crates/main/Cargo.toml | 2 +- crates/smtp/Cargo.toml | 2 +- resources/htx/crypto_disabled.htx | 1 + resources/htx/crypto_form.htx | 2 +- resources/htx/crypto_success.htx | 2 +- tests/Cargo.toml | 2 +- tests/resources/crypto/cert_mixed.pem | 35 +++ tests/resources/crypto/cert_pgp.der | Bin 0 -> 2240 bytes tests/resources/crypto/cert_pgp.pem | 69 ++++++ tests/resources/crypto/cert_smime.der | Bin 0 -> 882 bytes tests/resources/crypto/cert_smime.pem | 98 ++++++++ tests/resources/crypto/is_encrypted.txt | 101 ++++++++ tests/resources/test_config.toml | 3 +- tests/src/imap/mod.rs | 3 +- tests/src/jmap/auth_oauth.rs | 2 +- tests/src/jmap/crypto.rs | 308 ++++++++++++++++++++++++ tests/src/jmap/mod.rs | 6 +- 26 files changed, 735 insertions(+), 68 deletions(-) create mode 100644 resources/htx/crypto_disabled.htx create mode 100644 tests/resources/crypto/cert_mixed.pem create mode 100644 tests/resources/crypto/cert_pgp.der create mode 100644 tests/resources/crypto/cert_pgp.pem create mode 100644 tests/resources/crypto/cert_smime.der create mode 100644 tests/resources/crypto/cert_smime.pem create mode 100644 tests/resources/crypto/is_encrypted.txt create mode 100644 tests/src/jmap/crypto.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 8581af95..723ee4c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). +## [0.3.3] - 2023-08-02 + +### Added +- Encryption at rest with **S/MIME** and **PGP** support. +- Support for referencing context variables from dynamic values. + +### Changed + +### Fixed +- Support for PKCS8v1 ED25519 keys (#20). +- Automatic retry for import/export blob downloads (#14) + ## [0.3.2] - 2023-07-28 ### Added diff --git a/Cargo.lock b/Cargo.lock index 8ff192c5..efee2daf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2191,7 +2191,7 @@ dependencies = [ [[package]] name = "imap" -version = "0.3.2" +version = "0.3.3" dependencies = [ "ahash 0.8.3", "dashmap", @@ -2356,7 +2356,7 @@ dependencies = [ [[package]] name = "jmap" -version = "0.3.2" +version = "0.3.3" dependencies = [ "aes", "aes-gcm", @@ -2695,7 +2695,7 @@ dependencies = [ [[package]] name = "mail-server" -version = "0.3.2" +version = "0.3.3" dependencies = [ "directory", "imap", @@ -2844,6 +2844,16 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -3907,6 +3917,7 @@ dependencies = [ "js-sys", "log", "mime", + "mime_guess", "once_cell", "percent-encoding", "pin-project-lite", @@ -4544,7 +4555,7 @@ checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" [[package]] name = "smtp" -version = "0.3.2" +version = "0.3.3" dependencies = [ "ahash 0.8.3", "blake3", @@ -4867,7 +4878,7 @@ dependencies = [ [[package]] name = "stalwart-cli" -version = "0.3.2" +version = "0.3.3" dependencies = [ "clap", "console", @@ -4889,7 +4900,7 @@ dependencies = [ [[package]] name = "stalwart-install" -version = "0.3.2" +version = "0.3.3" dependencies = [ "base64 0.21.2", "clap", @@ -5600,6 +5611,15 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" +[[package]] +name = "unicase" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +dependencies = [ + "version_check", +] + [[package]] name = "unicode-bidi" version = "0.3.13" diff --git a/README.md b/README.md index 26557b9a..9495bacd 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,6 @@ [![](https://img.shields.io/discord/923615863037390889?label=Chat)](https://discord.gg/jtgtCNj66U) [![](https://img.shields.io/twitter/follow/stalwartlabs)](https://twitter.com/stalwartlabs) [![](https://img.shields.io/mastodon/follow/109929667531941122)](https://mastodon.social/@stalwartlabs) -[![](https://img.shields.io/badge/Follow-%40stalwartlabs-8A2BE2)](https://www.threads.net/@stalwartlabs) **Stalwart Mail Server** is an open-source mail server solution with JMAP, IMAP4, and SMTP support and a wide range of modern features. It is written in Rust and designed to be secure, fast, robust and scalable. @@ -35,6 +34,7 @@ Key features: - Email aliases, mailing lists, subaddressing and catch-all addresses support. - Integration with **OpenTelemetry** to enable monitoring, tracing, and performance analysis. - **Secure**: + - Encryption at rest with **S/MIME** and **PGP** support. - OAuth 2.0 [authorization code](https://www.rfc-editor.org/rfc/rfc8628) and [device authorization](https://www.rfc-editor.org/rfc/rfc8628) flows. - Access Control Lists (ACLs). - Rate limiting. diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 7fb7decf..bb3617df 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -5,7 +5,7 @@ authors = ["Stalwart Labs Ltd. "] license = "AGPL-3.0-only" repository = "https://github.com/stalwartlabs/cli" homepage = "https://github.com/stalwartlabs/cli" -version = "0.3.2" +version = "0.3.3" edition = "2021" readme = "README.md" resolver = "2" diff --git a/crates/imap/Cargo.toml b/crates/imap/Cargo.toml index 9ed1f597..1b6281b5 100644 --- a/crates/imap/Cargo.toml +++ b/crates/imap/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "imap" -version = "0.3.2" +version = "0.3.3" edition = "2021" resolver = "2" diff --git a/crates/install/Cargo.toml b/crates/install/Cargo.toml index c32da5bb..a43c371d 100644 --- a/crates/install/Cargo.toml +++ b/crates/install/Cargo.toml @@ -5,7 +5,7 @@ authors = ["Stalwart Labs Ltd. "] license = "AGPL-3.0-only" repository = "https://github.com/stalwartlabs/mail-server" homepage = "https://github.com/stalwartlabs/mail-server" -version = "0.3.2" +version = "0.3.3" edition = "2021" readme = "README.md" resolver = "2" diff --git a/crates/jmap/Cargo.toml b/crates/jmap/Cargo.toml index c8576025..de2b6edf 100644 --- a/crates/jmap/Cargo.toml +++ b/crates/jmap/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "jmap" -version = "0.3.2" +version = "0.3.3" edition = "2021" resolver = "2" diff --git a/crates/jmap/src/auth/oauth/mod.rs b/crates/jmap/src/auth/oauth/mod.rs index be6773d8..d3ddb8d6 100644 --- a/crates/jmap/src/auth/oauth/mod.rs +++ b/crates/jmap/src/auth/oauth/mod.rs @@ -213,6 +213,7 @@ impl TokenResponse { } } +#[derive(Debug)] pub struct FormData { fields: HashMap>, } diff --git a/crates/jmap/src/email/crypto.rs b/crates/jmap/src/email/crypto.rs index 168b6aa3..48968757 100644 --- a/crates/jmap/src/email/crypto.rs +++ b/crates/jmap/src/email/crypto.rs @@ -21,7 +21,7 @@ * for more details. */ -use std::{borrow::Cow, collections::BTreeSet}; +use std::{borrow::Cow, collections::BTreeSet, fmt::Display}; use aes::cipher::{block_padding::Pkcs7, BlockEncryptMut, KeyIvInit}; use jmap_proto::types::{collection::Collection, property::Property}; @@ -53,6 +53,7 @@ const CRYPT_HTML_HEADER: &str = include_str!("../../../../resources/htx/crypto_h const CRYPT_HTML_FOOTER: &str = include_str!("../../../../resources/htx/crypto_footer.htx"); const CRYPT_HTML_FORM: &str = include_str!("../../../../resources/htx/crypto_form.htx"); const CRYPT_HTML_SUCCESS: &str = include_str!("../../../../resources/htx/crypto_success.htx"); +const CRYPT_HTML_DISABLED: &str = include_str!("../../../../resources/htx/crypto_disabled.htx"); const CRYPT_HTML_ERROR: &str = include_str!("../../../../resources/htx/crypto_error.htx"); #[derive(Debug)] @@ -75,9 +76,9 @@ pub enum EncryptionMethod { #[derive(Debug, serde::Serialize, serde::Deserialize)] pub struct EncryptionParams { - method: EncryptionMethod, - algo: Algorithm, - certs: Vec>, + pub method: EncryptionMethod, + pub algo: Algorithm, + pub certs: Vec>, } #[async_trait::async_trait] @@ -496,7 +497,7 @@ fn try_parse_pem(bytes: &[u8]) -> Result>) Ok(method.map(|method| (method, certs))) } -impl Serialize for EncryptionParams { +impl Serialize for &EncryptionParams { fn serialize(self) -> Vec { let len = bincode::serialized_size(&self).unwrap_or_default(); let mut buf = Vec::with_capacity(len as usize + 1); @@ -529,7 +530,7 @@ impl Deserialize for EncryptionParams { } } -impl ToBitmaps for EncryptionParams { +impl ToBitmaps for &EncryptionParams { fn to_bitmaps(&self, _: &mut Vec, _: u8, _: bool) { unreachable!() } @@ -538,7 +539,12 @@ impl ToBitmaps for EncryptionParams { impl JMAP { // Code authorization flow, handles an authorization request pub async fn handle_crypto_update(&self, req: &mut HttpRequest) -> HttpResponse { - let response = match *req.method() { + let mut response = String::with_capacity( + CRYPT_HTML_HEADER.len() + CRYPT_HTML_FOOTER.len() + CRYPT_HTML_FORM.len(), + ); + response.push_str(&CRYPT_HTML_HEADER.replace("@@@", "/crypto")); + + match *req.method() { hyper::Method::POST => { // Parse form let form = match FormData::from_request(req, 1024 * 1024).await { @@ -546,54 +552,43 @@ impl JMAP { Err(err) => return err, }; - if let Err(error) = self.validate_form(form).await { - let mut response = String::with_capacity( - CRYPT_HTML_HEADER.len() - + CRYPT_HTML_FOOTER.len() - + CRYPT_HTML_ERROR.len() - + error.len(), - ); - - response.push_str(&CRYPT_HTML_HEADER.replace("@@@", "/crypto")); - response.push_str(&CRYPT_HTML_ERROR.replace("@@@", &error)); - response.push_str(CRYPT_HTML_FOOTER); - - response - } else { - let mut response = String::with_capacity( - CRYPT_HTML_HEADER.len() - + CRYPT_HTML_FOOTER.len() - + CRYPT_HTML_SUCCESS.len(), - ); - - response.push_str(&CRYPT_HTML_HEADER.replace("@@@", "/crypto")); - response.push_str(CRYPT_HTML_SUCCESS); - response.push_str(CRYPT_HTML_FOOTER); - - response + match self.validate_form(form).await { + Ok(Some(params)) => { + response.push_str( + &CRYPT_HTML_SUCCESS + .replace( + "$$$", + format!("{} ({})", params.method, params.algo).as_str(), + ) + .replace("@@@", params.certs.len().to_string().as_str()), + ); + } + Ok(None) => { + response.push_str(CRYPT_HTML_DISABLED); + } + Err(error) => { + response.push_str(&CRYPT_HTML_ERROR.replace("@@@", &error)); + } } } hyper::Method::GET => { - let mut response = String::with_capacity( - CRYPT_HTML_HEADER.len() + CRYPT_HTML_FOOTER.len() + CRYPT_HTML_FORM.len(), - ); - - response.push_str(&CRYPT_HTML_HEADER.replace("@@@", "/crypto")); response.push_str(CRYPT_HTML_FORM); - response.push_str(CRYPT_HTML_FOOTER); - - response } _ => unreachable!(), }; + response.push_str(CRYPT_HTML_FOOTER); + HtmlResponse::new(response).into_http_response() } - async fn validate_form(&self, mut form: FormData) -> Result<(), Cow> { - if let (Some(certificate), Some(email), Some(password), Some(encryption)) = ( - form.remove_bytes("certificate"), + async fn validate_form( + &self, + mut form: FormData, + ) -> Result, Cow> { + let certificate = form.remove_bytes("certificate"); + if let (Some(email), Some(password), Some(encryption)) = ( form.get("email"), form.get("password"), form.get("encryption"), @@ -601,7 +596,8 @@ impl JMAP { // Validate fields if email.is_empty() || password.is_empty() { return Err(Cow::from("Please enter your login and password")); - } else if encryption != "disable" && certificate.is_empty() { + } else if encryption != "disable" && certificate.as_ref().map_or(true, |c| c.is_empty()) + { return Err(Cow::from("Please select one or more certificates")); } @@ -611,7 +607,8 @@ impl JMAP { .await .ok_or_else(|| Cow::from("Invalid login or password"))?; if encryption != "disable" { - let (method, certs) = try_parse_certs(certificate).map_err(Cow::from)?; + let (method, certs) = + try_parse_certs(certificate.unwrap_or_default()).map_err(Cow::from)?; let algo = match (encryption, method) { ("pgp-256", EncryptionMethod::PGP) => Algorithm::Aes256, ("pgp-128", EncryptionMethod::PGP) => Algorithm::Aes128, @@ -645,10 +642,12 @@ impl JMAP { .with_account_id(token.primary_id()) .with_collection(Collection::Principal) .update_document(0) - .value(Property::Parameters, params, F_VALUE); + .value(Property::Parameters, ¶ms, F_VALUE); self.write_batch(batch).await.map_err(|_| { Cow::from("Failed to save encryption parameters, please try again later") })?; + + Ok(Some(params)) } else { // Remove encryption params let mut batch = BatchBuilder::new(); @@ -660,11 +659,28 @@ impl JMAP { self.write_batch(batch).await.map_err(|_| { Cow::from("Failed to save encryption parameters, please try again later") })?; + Ok(None) } - - Ok(()) } else { Err(Cow::from("Missing form parameters")) } } } + +impl Display for EncryptionMethod { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + EncryptionMethod::PGP => write!(f, "PGP"), + EncryptionMethod::SMIME => write!(f, "S/MIME"), + } + } +} + +impl Display for Algorithm { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Algorithm::Aes128 => write!(f, "AES-128"), + Algorithm::Aes256 => write!(f, "AES-256"), + } + } +} diff --git a/crates/main/Cargo.toml b/crates/main/Cargo.toml index 505f910c..a06b5ec8 100644 --- a/crates/main/Cargo.toml +++ b/crates/main/Cargo.toml @@ -7,7 +7,7 @@ homepage = "https://stalw.art" keywords = ["imap", "jmap", "smtp", "email", "mail", "server"] categories = ["email"] license = "AGPL-3.0-only" -version = "0.3.2" +version = "0.3.3" edition = "2021" resolver = "2" diff --git a/crates/smtp/Cargo.toml b/crates/smtp/Cargo.toml index 9529ddff..62d9a43b 100644 --- a/crates/smtp/Cargo.toml +++ b/crates/smtp/Cargo.toml @@ -7,7 +7,7 @@ homepage = "https://stalw.art/smtp" keywords = ["smtp", "email", "mail", "server"] categories = ["email"] license = "AGPL-3.0-only" -version = "0.3.2" +version = "0.3.3" edition = "2021" resolver = "2" diff --git a/resources/htx/crypto_disabled.htx b/resources/htx/crypto_disabled.htx new file mode 100644 index 00000000..b102ca37 --- /dev/null +++ b/resources/htx/crypto_disabled.htx @@ -0,0 +1 @@ +

Encryption at rest disabled

Messages will now be stored in plain text on the server..

\ No newline at end of file diff --git a/resources/htx/crypto_form.htx b/resources/htx/crypto_form.htx index e60617a8..a6c14847 100644 --- a/resources/htx/crypto_form.htx +++ b/resources/htx/crypto_form.htx @@ -1 +1 @@ -

Enable encryption at rest for your Stalwart Mail Server account

Select Certificate...
Cancel \ No newline at end of file +

Enable encryption at rest for your Stalwart Mail Server account

Select Certificate...
Cancel \ No newline at end of file diff --git a/resources/htx/crypto_success.htx b/resources/htx/crypto_success.htx index 54b57472..0cb9ceb7 100644 --- a/resources/htx/crypto_success.htx +++ b/resources/htx/crypto_success.htx @@ -1 +1 @@ -

Encryption settings updated successfully

You can now close this window.

\ No newline at end of file +

$$$ encryption at rest is now enabled

@@@ certificate(s) were imported.

\ No newline at end of file diff --git a/tests/Cargo.toml b/tests/Cargo.toml index 04b2ef7c..f7eee270 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -37,7 +37,7 @@ serde = { version = "1.0", features = ["derive"]} serde_json = "1.0" tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } -reqwest = { version = "0.11", default-features = false, features = ["rustls-tls-webpki-roots"]} +reqwest = { version = "0.11", default-features = false, features = ["rustls-tls-webpki-roots", "multipart"]} bytes = "1.4.0" futures = "0.3" ece = "2.2" diff --git a/tests/resources/crypto/cert_mixed.pem b/tests/resources/crypto/cert_mixed.pem new file mode 100644 index 00000000..b4094d6e --- /dev/null +++ b/tests/resources/crypto/cert_mixed.pem @@ -0,0 +1,35 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +xjMEZMYfNhYJKwYBBAHaRw8BAQdAYyTN1HzqapLw8xwkCGwa0OjsgT/JqhcB/+Dy +Ga1fsBrNG0pvaG4gRG9lIDxqb2huQGV4YW1wbGUub3JnPsKJBBMWCAAxFiEEg836 +pwbXpuQ/THMtpJwd4oBfIrUFAmTGHzYCGwMECwkIBwUVCAkKCwUWAgMBAAAKCRCk +nB3igF8itYhyAQD2jEdeYa3gyQ47X9YWZTK1wEJkN8W9//V1fYl2XQwqlQEA0qBv +Ai6nUh99oDw+/zQ8DFIKdeb5Ti4tu/X58PdpiQ7OOARkxh82EgorBgEEAZdVAQUB +AQdAvXz2FbFN0DovQF/ACnZyczTsSIQp0mvmF1PE+aijbC8DAQgHwngEGBYIACAW +IQSDzfqnBtem5D9Mcy2knB3igF8itQUCZMYfNgIbDAAKCRCknB3igF8itRnoAQC3 +GzPmgx7TnB+SexPuJV/DoKSMJ0/X+hbEFcZkulxaDQEAh+xiJCvf+ZNAKw6kFhsL +UuZhEDktxnY6Ehz3aB7FawA= +=KGrr +-----END PGP PUBLIC KEY BLOCK----- + +-----BEGIN CERTIFICATE----- +MIIDbjCCAlagAwIBAgIUZ4K0WXNSS8H0cUcZavD9EYqqTAswDQYJKoZIhvcNAQEN +BQAwLTErMCkGA1UEAxMiU2FtcGxlIExBTVBTIENlcnRpZmljYXRlIEF1dGhvcml0 +eTAgFw0xOTExMjAwNjU0MThaGA8yMDUyMDkyNzA2NTQxOFowGTEXMBUGA1UEAxMO +QWxpY2UgTG92ZWxhY2UwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDD +7q35ZdG2JAzzJGNZDZ9sV7AKh0hlRfoFjTZN5m4RegQAYSyag43ouWi1xRN0avf0 +UTYrwjK04qRdV7GzCACoEKq/xiNUOsjfJXzbCublN3fZMOXDshKKBqThlK75SjA9 +Czxg7ejGoiY/iidk0e91neK30SCCaBTJlfR2ZDrPk73IPMeksxoTatfF9hw9dDA+ +/Hi1yptN/aG0Q/s9icFrxr6y2zQXsjuQPmjMZgj10aD9cazWVgRYCgflhmA0V1uQ +l1wobYU8DAVxVn+GgabqyjGQMoythIK0Gn5+ofwxXXUM/zbU+g6+1ISdoXxRRFtq +2GzbIqkAHZZQm+BbnFrhAgMBAAGjgZcwgZQwDAYDVR0TAQH/BAIwADAeBgNVHREE +FzAVgRNhbGljZUBzbWltZS5leGFtcGxlMBMGA1UdJQQMMAoGCCsGAQUFBwMEMA8G +A1UdDwEB/wQFAwMHoAAwHQYDVR0OBBYEFKwuVFqk/VUYry7oZkQ40SXR1wB5MB8G +A1UdIwQYMBaAFLdSTXPAiD2yw3paDPOU9/eAonfbMA0GCSqGSIb3DQEBDQUAA4IB +AQB76o4Yz7yrVSFcpXqLrcGtdI4q93aKCXECCCzNQLp4yesh6brqaZHNJtwYcJ5T +qbUym9hJ70iJE4jGNN+yAZR1ltte0HFKYIBKM4EJumG++2hqbUaLz4tl06BHaQPC +v/9NiNY7q9R9c/B6s1YzHhwqkWht2a+AtgJ4BkpG+g+MmZMQV/Ao7RwLFKJ9OlMW +LBmEXFcpIJN0HpPasT0nEl/MmotSu+8RnClAi3yFfyTKb+8rD7VxuyXetqDZ6dU/ +9/iqD/SZS7OQIjywtd343mACz3B1RlFxMHSA6dQAf2btGumqR0KiAp3KkYRAePoa +JqYkB7Zad06ngFl0G0FHON+7 +-----END CERTIFICATE----- diff --git a/tests/resources/crypto/cert_pgp.der b/tests/resources/crypto/cert_pgp.der new file mode 100644 index 0000000000000000000000000000000000000000..3689a7d92acb85b0f8b6e8c62d3a1047538e8d2a GIT binary patch literal 2240 zcmV;x2tW76!A%5Y#vcg*5CG8!kp#8-CsPOo*_2@F9bczmJS^C6;#U0uXREM}3N%_4 zGpex$Pt1HuU?8-Qb#376^@lqX)&3k~wJSv(_SF4{xmNevkxo5*q7RD=Roui0Ctme% z-lTxZzy2D$v9^Q-Ogeq=H6eH9VU6=1?S>g zH`x+bDu?rWN~LZS3KK6Q0-VNrf-_8?t6g^k$J>b-y_i#{udjtM=E|=Z2BN=QOMS2B zB}w=;hAwREW4na~ep_y_Mm77f0$Afy-_A%FCqPW=>@@X#Sv&ZQj7oUo-K%$eQn}m) zVi{X&HQRI_jwPnIM%<$~2Ze!;4#?R~4;|ITa_txGbl`E4FS+ET8!qUw;(`>Q5J7fU zi8$U0>(Ys`Z&Oc&Y5-;vJh47vznB($tHIRmcXQ}NbNGf#3*4^POD=JC3eUP6KI`u@ z&ohGh4zTt5ge~V?2mXVE6O&oMguM!2U((!a!cOPaPF44rXq}*n_IKzhr>_j|C~BGE^XQ6-mip z%IGip=Isb*TV<9m^!^C&kHM_Jv<<<_z+U}>qlK%<@@Ms1de6?^JO71f>FVFhGW3++ zg+gheDfIY{jiVQYj7uzn+br*VF3(SoZuq4NLT}5Z3m+(Ya9y9(Bd?bl)?-eq9Qx=b zC4^|}+9uoDK?SVGqw#ihs>o5accN~3)fZEPfp3d7!1b&l8e4n!1UZ#?h5|3*SV_a8ZLTs*aWc?opQ8394z?Y zi=pit0E@dNJmlb6*nR{F%0tcNJ&nH#Xl7_JFpS z`~XX3^HuS7aMCbU25s0~EzWOyh&$wOu-buwct&8~!sk>7_7`zoWdoEwZ*>-&US|j_ zFqjK`I=jaj#Jwc7bkgHI4f+u-?lJKM*otzC!gzssRh~_7`f2O#nELZwnq~0%RgJ(5 zcylzF>~hj?}~Y0{tE#*Z_mRKG0$8Yn_=)1)v0O z{;qh0P7{WlqQl50Tx{L5@yr|-hUJ44BuVFxTK8;8-tZGtfq-_HB|ZvIS77bgJAV75uYlg z$+h^F6!`7V(_k{6(t62ch7nh@B6LKM+*8G0L>kTv8QHw~*^7zGX<&=gw?_lUJrI}@ z!Beb#)M_vF;t&9pMV=SCLw6w!u;wy`#_cSNi;9*W>Waq|j%Mq_O*(?s z!l52c`>iGcUJX7L#fY>#mW@GyPsn9*gR^vmEO~Hj8jacT8kYIal#R;qwqff%BU8V- zzl9dI(!lGe>sxiP!J6@jU`(nq8P8n98F^F5E3Q#6T{V#Yp>-OqupDi-!fiv@-H%*Q z(Z3DgKN4rjX)DK>HtM^zk_(LOaaG9}y9eur;`eVdTucRXH;$3@2$XB{WpB-c@9q6L zXO!p*wZo`-Rx2I;0Sn>%n-T_)Va%^8YAkZF%qMFHwC1yq$v8zUo7B zv|$$t8TpWjx=M6B?;@Z+{q3a$hJTSPkf zPbH;MM?8oCsKBWt(^^D5&m5x@!l$8shd!i&XPZUq^Rv4#!nC_-MY3@zZ12s<3$>2h zuERt<^c38F^)Aw5fdbAD#*DXTd#nheCiO4Wg_2D`0H*hwZ@L5@$HXP=GN`@gkzNWl zef7G2JQ}Ep{C!GMSZ1$kaA52HJganX71?M199}0Y#7I97es!P^Ftq89g@J_Hu1HR% OMGL?LNAwu@ws^4QyExMT literal 0 HcmV?d00001 diff --git a/tests/resources/crypto/cert_pgp.pem b/tests/resources/crypto/cert_pgp.pem new file mode 100644 index 00000000..a74caf94 --- /dev/null +++ b/tests/resources/crypto/cert_pgp.pem @@ -0,0 +1,69 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +=0TDv +-----END PGP PUBLIC KEY BLOCK----- + +-----BEGIN PGP PUBLIC KEY BLOCK----- + +xjMEZMYfNhYJKwYBBAHaRw8BAQdAYyTN1HzqapLw8xwkCGwa0OjsgT/JqhcB/+Dy +Ga1fsBrNG0pvaG4gRG9lIDxqb2huQGV4YW1wbGUub3JnPsKJBBMWCAAxFiEEg836 +pwbXpuQ/THMtpJwd4oBfIrUFAmTGHzYCGwMECwkIBwUVCAkKCwUWAgMBAAAKCRCk +nB3igF8itYhyAQD2jEdeYa3gyQ47X9YWZTK1wEJkN8W9//V1fYl2XQwqlQEA0qBv +Ai6nUh99oDw+/zQ8DFIKdeb5Ti4tu/X58PdpiQ7OOARkxh82EgorBgEEAZdVAQUB +AQdAvXz2FbFN0DovQF/ACnZyczTsSIQp0mvmF1PE+aijbC8DAQgHwngEGBYIACAW +IQSDzfqnBtem5D9Mcy2knB3igF8itQUCZMYfNgIbDAAKCRCknB3igF8itRnoAQC3 +GzPmgx7TnB+SexPuJV/DoKSMJ0/X+hbEFcZkulxaDQEAh+xiJCvf+ZNAKw6kFhsL +UuZhEDktxnY6Ehz3aB7FawA= +=KGrr +-----END PGP PUBLIC KEY BLOCK----- + + + + diff --git a/tests/resources/crypto/cert_smime.der b/tests/resources/crypto/cert_smime.der new file mode 100644 index 0000000000000000000000000000000000000000..96e3472d4da73c164ad670b37a45c209eeaa4389 GIT binary patch literal 882 zcmXqLV$L&YVhUTp%*4pVB$D2=C9*ik`{0*Ccgd^|e+9c%`EVQXvTO zY&|0@19KB2KLb#li>Zl`k>T*WwLeoYZd2j;tdbnbJ3l9U16R98s_QS-UNhfkd4g3e z42e3kntNaD%-DKVxFqZQmq0V^Lq=O3Er|`^xS4}tg}|!)$CN{?PTW_mxy|+Lsd@QL zgQtf#33ah8c{pX=PcH*oZkvR+FODrzv+q(*x%j?x?xXD&6`C?cPEP$&mSS~&^4=3R z$CqrD63)7Q^qY)riGkgpimj(+`~F?H#rd~w=fUh_`!?M+5#MAz!7k%W8pqd*3;q_a zxfaF}!NvZxEx{x_dcyP=joeln9@fIJ`nJYpuTB|GFzQ*`(zHdYu5RHU!`M=u|7KTy z@$I|PGIwE3pi6Ytjhx#`D;Z>`1O^-?RqQDz_vl2>KnG2mk3&}L&~Wo2h(G2jOY@`H?E zWoBkyz+fN?65wMIV-Z=S7ZSDPZ>Yq2y%%XN78g}7UT3H@kOxUCvq%_-HHd5v@-05l zVY}&YRTR(XDc`?0EGoZ^9CyH^1B^RHhU!;+66g1<4poddKPDX2DM()k^4cnM1*u30+ z@%PM}ED-)deJpz myVVJV_8!G literal 0 HcmV?d00001 diff --git a/tests/resources/crypto/cert_smime.pem b/tests/resources/crypto/cert_smime.pem new file mode 100644 index 00000000..8913a16f --- /dev/null +++ b/tests/resources/crypto/cert_smime.pem @@ -0,0 +1,98 @@ +-----BEGIN CERTIFICATE----- +MIIHbTCCBVWgAwIBAgIQFxA+3j2KHLXKBlGT58pDazANBgkqhkiG9w0BAQsFADBr +MQswCQYDVQQGEwJJVDEOMAwGA1UEBwwFTWlsYW4xIzAhBgNVBAoMGkFjdGFsaXMg +Uy5wLkEuLzAzMzU4NTIwOTY3MScwJQYDVQQDDB5BY3RhbGlzIEF1dGhlbnRpY2F0 +aW9uIFJvb3QgQ0EwHhcNMjAwNzA2MDg0NTQ3WhcNMzAwOTIyMTEyMjAyWjCBgTEL +MAkGA1UEBhMCSVQxEDAOBgNVBAgMB0JlcmdhbW8xGTAXBgNVBAcMEFBvbnRlIFNh +biBQaWV0cm8xFzAVBgNVBAoMDkFjdGFsaXMgUy5wLkEuMSwwKgYDVQQDDCNBY3Rh +bGlzIENsaWVudCBBdXRoZW50aWNhdGlvbiBDQSBHMzCCAiIwDQYJKoZIhvcNAQEB +BQADggIPADCCAgoCggIBAO3mh5ahwaS27cJCVfc/Dw8iYF8T4KZDiIZJkXkcGy8a +UA/cRgHu9ro6hsxRYe/ED4AIcSlarRh82HqtFSVQs4ZwikQW1V/icCIS91C2IVAG +a1YlKfedqgweqky+bBniUvRevVT0keZOqRTcO5hw007dL6FhYNmlZBt5IaJs1V6I +niRjokOHR++qWgrUGy5LefY6ACs9gZ8Bi0OMK9PZ37pibeQCsdmMRytl4Ej7JVWe +M/BtNIIprHwO1LY0/8InpGOmdG+5LC6xHLzg53B0HvVUqzUQNePUhNwJZFmmTP46 +FXovxmH4/SuY5IkXop0eJqjN+dxRHHizngYUk1EaTHUOcLFy4vQ0kxgbjb+GsNg6 +M2/6gZZIRk78JPdpotIwHnBNtkp9wPVH61NqdcP7kbPkyLXkNMTtAfydpmNnGqqH +LEvUrK4iBpUPG9C09KOjm9OyhrT2uf5SLzJsee9g79r/rw4hAgcsZtR3YI6fCbRO +JncmD+hgbHCck+9TWcNc1x5xZMgm8UXmoPamkkfceAlVV49QQ5jUTgqneTQHyF1F +2ExXmf47pEIoJMVxloRIXywQuB2uqcIs8/X6tfsMDynFmhfT/0mTrgQ6xt9DIsgm +WuuhvZhLReWS7oeKxnyqscuGeTMXnLs7fjGZq0inyhnlznhA/4rl+WdNjNaO4jEv +AgMBAAGjggH0MIIB8DAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFFLYiDrI +n3hm7YnzezhwlMkCAjbQMEEGCCsGAQUFBwEBBDUwMzAxBggrBgEFBQcwAYYlaHR0 +cDovL29jc3AwNS5hY3RhbGlzLml0L1ZBL0FVVEgtUk9PVDBFBgNVHSAEPjA8MDoG +BFUdIAAwMjAwBggrBgEFBQcCARYkaHR0cHM6Ly93d3cuYWN0YWxpcy5pdC9hcmVh +LWRvd25sb2FkMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcDBDCB4wYDVR0f +BIHbMIHYMIGWoIGToIGQhoGNbGRhcDovL2xkYXAwNS5hY3RhbGlzLml0L2NuJTNk +QWN0YWxpcyUyMEF1dGhlbnRpY2F0aW9uJTIwUm9vdCUyMENBLG8lM2RBY3RhbGlz +JTIwUy5wLkEuJTJmMDMzNTg1MjA5NjcsYyUzZElUP2NlcnRpZmljYXRlUmV2b2Nh +dGlvbkxpc3Q7YmluYXJ5MD2gO6A5hjdodHRwOi8vY3JsMDUuYWN0YWxpcy5pdC9S +ZXBvc2l0b3J5L0FVVEgtUk9PVC9nZXRMYXN0Q1JMMB0GA1UdDgQWBBS+l6mqhL+A +vxBTfQky+eEuMhvPdzAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQELBQADggIB +ACab5xtZDXSzEgPp51X3hICFzULDO2EcV8em5hLfSCKxZR9amCnjcODVfMbaKfdU +ZXtevMIIZmHgkz9dBan7ijGbJXjZCPP29zwZGSyCjpfadg5s9hnNCN1r3DGwIHfy +LgbcfffDyV/2wW+XTGbhldnazZsX892q+srRmC8XnX4ygg+eWL/AkHDenvbFuTlJ +vUyd5I7e1nb3dYXMObPu24ZTQ9/K1hSQbs7pqecaptTUjoIDpBUpSp4Us+h1I4MA +WonemKYoPS9f0y65JrRCKcfsKSI+1kwPSanDDMiydKzeo46XrS0hlA5NzQjqUJ7U +suGvPtDvknqc0v03nNXBnUjejYtvwO3sEDXdUW5m9kjNqlQZXzdHumZJVqPUGKTW +cn9Hf3d7qbCmmxPXjQoNUuHg56fLCanZWkEO4SP1GAgIA7SyJu/yffv0ts7sBFrS +TD3L2mCAXM3Y8BfblvvDSf2bvySm/fPe9brmuzrCXsTxUQc1+/z5ydvzV3E3cLnU +oSXP6XfXNyEVO6sPkcUSnISHM798xLkCTB5EkjPCjPE2zs4v9L9JVOkkskvW6RnW +WccdfR3fELNHL/kep8re6IbbYs8Hn5GM0Ohs8CMDPYEox+QX/6/SnOfyaqqSilBo +nMQBstsymBBgdEKO+tTHHCMnJQVvZn7jRQ20wXgxMrvN +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFuzCCA6OgAwIBAgIIVwoRl0LE48wwDQYJKoZIhvcNAQELBQAwazELMAkGA1UE +BhMCSVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8w +MzM1ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290 +IENBMB4XDTExMDkyMjExMjIwMloXDTMwMDkyMjExMjIwMlowazELMAkGA1UEBhMC +SVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8wMzM1 +ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290IENB +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAp8bEpSmkLO/lGMWwUKNv +UTufClrJwkg4CsIcoBh/kbWHuUA/3R1oHwiD1S0eiKD4j1aPbZkCkpAW1V8IbInX +4ay8IMKx4INRimlNAJZaby/ARH6jDuSRzVju3PvHHkVH3Se5CAGfpiEd9UEtL0z9 +KK3giq0itFZljoZUj5NDKd45RnijMCO6zfB9E1fAXdKDa0hMxKufgFpbOr3JpyI/ +gCczWw63igxdBzcIy2zSekciRDXFzMwujt0q7bd9Zg1fYVEiVRvjRuPjPdA1Yprb +rxTIW6HMiRvhMCb8oJsfgadHHwTrozmSBp+Z07/T6k9QnBn+locePGX2oxgkg4YQ +51Q+qDp2JE+BIcXjDwL4k5RHILv+1A7TaLndxHqEguNTVHnd25zS8gebLra8Pu2F +be8lEfKXGkJh90qX6IuxEAf6ZYGyojnP9zz/GPvG8VqLWeICrHuS0E4UT1lF9gxe +KF+w6D9Fz8+vm2/7hNN3WpVvrJSEnu68wEqPSpP4RCHiMUVhUE4Q2OM1fEwZtN4F +v6MGn8i1zeQf1xcGDXqVdFUNaBr8EBtiZJ1t4JWgw5QHVw0U5r0F+7if5t+L4sbn +fpb2U8WANFAoWPASUHEXMLrmeGO89LKtmyuy/uE5jF66CyCU3nuDuP/jVo23Eek7 +jPKxwV2dpAtMK9myGPW1n0sCAwEAAaNjMGEwHQYDVR0OBBYEFFLYiDrIn3hm7Ynz +ezhwlMkCAjbQMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUUtiIOsifeGbt +ifN7OHCUyQICNtAwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBCwUAA4ICAQAL +e3KHwGCmSUyIWOYdiPcUZEim2FgKDk8TNd81HdTtBjHIgT5q1d07GjLukD0R0i70 +jsNjLiNmsGe+b7bAEzlgqqI0JZN1Ut6nna0Oh4lScWoWPBkdg/iaKWW+9D+a2fDz +WochcYBNy+A4mz+7+uAwTc+G02UQGRjRlwKxK3JCaKygvU5a2hi/a5iB0P2avl4V +SM0RFbnAKVy06Ij3Pjaut2L9HmLecHgQHEhb2rykOLpn7VU+Xlff1ANATIGk0k9j +pwlCCRT8AKnCgHNPLsBA2RF7SOp6AsDT6ygBJlh0wcBzIm2Tlf05fbsq4/aC4yyX +X04fkZT6/iyj2HYauE2yOE+b+h1IYHkm4vP9qdCa6HCPSXrW5b0KDtst842/6+Ok +fcvHlXHo2qN8xcL4dJIEG4aspCJTQLas/kx2z/uUMsA1n3Y/buWQbqCmJqK4LL7R +K4X9p2jIugErsWx0Hbhzlefut8cl8ABMALJ+tguLHPPAUJ4lueAI3jZm/zel0btU +ZCzJJ7VLkn5l/9Mt4blOvH+kQSGQQXemOR/qnuOf0GZvBeyqdn6/axag67XH/JJU +LysRJyU3eExRarDzzFhdFPFqSBX/wge2sY0PjlxQRrM9vwGYT7JZVEc+NHt4bVaT +LnPqZih4zR0Uv6CPLy64Lo7yFIrM6bV8+2ydDKXhlg== +-----END CERTIFICATE----- + + +-----BEGIN CERTIFICATE----- +MIIDbjCCAlagAwIBAgIUZ4K0WXNSS8H0cUcZavD9EYqqTAswDQYJKoZIhvcNAQEN +BQAwLTErMCkGA1UEAxMiU2FtcGxlIExBTVBTIENlcnRpZmljYXRlIEF1dGhvcml0 +eTAgFw0xOTExMjAwNjU0MThaGA8yMDUyMDkyNzA2NTQxOFowGTEXMBUGA1UEAxMO +QWxpY2UgTG92ZWxhY2UwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDD +7q35ZdG2JAzzJGNZDZ9sV7AKh0hlRfoFjTZN5m4RegQAYSyag43ouWi1xRN0avf0 +UTYrwjK04qRdV7GzCACoEKq/xiNUOsjfJXzbCublN3fZMOXDshKKBqThlK75SjA9 +Czxg7ejGoiY/iidk0e91neK30SCCaBTJlfR2ZDrPk73IPMeksxoTatfF9hw9dDA+ +/Hi1yptN/aG0Q/s9icFrxr6y2zQXsjuQPmjMZgj10aD9cazWVgRYCgflhmA0V1uQ +l1wobYU8DAVxVn+GgabqyjGQMoythIK0Gn5+ofwxXXUM/zbU+g6+1ISdoXxRRFtq +2GzbIqkAHZZQm+BbnFrhAgMBAAGjgZcwgZQwDAYDVR0TAQH/BAIwADAeBgNVHREE +FzAVgRNhbGljZUBzbWltZS5leGFtcGxlMBMGA1UdJQQMMAoGCCsGAQUFBwMEMA8G +A1UdDwEB/wQFAwMHoAAwHQYDVR0OBBYEFKwuVFqk/VUYry7oZkQ40SXR1wB5MB8G +A1UdIwQYMBaAFLdSTXPAiD2yw3paDPOU9/eAonfbMA0GCSqGSIb3DQEBDQUAA4IB +AQB76o4Yz7yrVSFcpXqLrcGtdI4q93aKCXECCCzNQLp4yesh6brqaZHNJtwYcJ5T +qbUym9hJ70iJE4jGNN+yAZR1ltte0HFKYIBKM4EJumG++2hqbUaLz4tl06BHaQPC +v/9NiNY7q9R9c/B6s1YzHhwqkWht2a+AtgJ4BkpG+g+MmZMQV/Ao7RwLFKJ9OlMW +LBmEXFcpIJN0HpPasT0nEl/MmotSu+8RnClAi3yFfyTKb+8rD7VxuyXetqDZ6dU/ +9/iqD/SZS7OQIjywtd343mACz3B1RlFxMHSA6dQAf2btGumqR0KiAp3KkYRAePoa +JqYkB7Zad06ngFl0G0FHON+7 +-----END CERTIFICATE----- diff --git a/tests/resources/crypto/is_encrypted.txt b/tests/resources/crypto/is_encrypted.txt new file mode 100644 index 00000000..835e3002 --- /dev/null +++ b/tests/resources/crypto/is_encrypted.txt @@ -0,0 +1,101 @@ +Subject: TRUE +Content-Type: multipart/encrypted; + protocol="application/pgp-encrypted"; + boundary="17778885806d6b28_46555a54adda6fa5_d83705f326a6b951" + +body +--- +Subject: FALSE +Content-Type: multipart/signed; + protocol="application/pkcs7-signature"; + boundary="17778885806d6b28_46555a54adda6fa5_d83705f326a6b951" + +body +--- +Subject: TRUE +Content-Type: application/pkcs7-mime; + name="smime.p7m"; + smime-type=enveloped-data +Content-Disposition: attachment; + filename="smime.p7m" +Content-Transfer-Encoding: base64 + +body +--- +Subject: TRUE +Content-Type: application/pkcs7-signature; + name="smime.p7s"; smime-type="signed-data" +Content-Disposition: attachment; + filename="smime.p7s" +Content-Transfer-Encoding: base64 + +body +--- +Subject: TRUE +Content-Type: application/octet-stream; + name="smime.p7m" +Content-Disposition: attachment; + filename="smime.p7m" +Content-Transfer-Encoding: base64 + +body +--- +Subject: TRUE +Content-Type: application/octet-stream; + name="smime.p7m" +Content-Transfer-Encoding: base64 + +body +--- +Subject: TRUE +Content-Type: application/octet-stream +Content-Disposition: attachment; + filename="smime.p7m" +Content-Transfer-Encoding: base64 + +body +--- +Subject: TRUE +Content-Type: application/octet-stream +Content-Disposition: attachment; + filename="smime.p7s" +Content-Transfer-Encoding: base64 + +body +--- +Subject: TRUE +Content-Type: application/octet-stream +Content-Disposition: attachment; + filename="smime.p7c" +Content-Transfer-Encoding: base64 + +body +--- +Subject: TRUE +Content-Type: application/octet-stream +Content-Disposition: attachment; + filename="smime.p7z" +Content-Transfer-Encoding: base64 + +body +--- +Subject: FALSE +Content-Type: application/octet-stream +Content-Disposition: attachment; + filename="file.gz" +Content-Transfer-Encoding: base64 + +body +--- +Subject: FALSE +Content-Type: application/octet-stream +Content-Disposition: attachment +Content-Transfer-Encoding: base64 + +body +--- +Subject: FALSE +Content-Type: multipart/mixed; + boundary="17778885806d6b28_46555a54adda6fa5_d83705f326a6b951" + +body diff --git a/tests/resources/test_config.toml b/tests/resources/test_config.toml index 06345221..55eef7e1 100644 --- a/tests/resources/test_config.toml +++ b/tests/resources/test_config.toml @@ -184,7 +184,8 @@ description = "Support Team" [oauth] key = "parerga_und_paralipomena" -oauth.auth.max-attempts = 1 +[oauth.auth] +max-attempts = 1 [oauth.expiry] user-code = "1s" diff --git a/tests/src/imap/mod.rs b/tests/src/imap/mod.rs index f878224b..18dec9af 100644 --- a/tests/src/imap/mod.rs +++ b/tests/src/imap/mod.rs @@ -207,7 +207,8 @@ remote-domains = ["remote.org", "foobar.com", "test.com", "other_domain.com"] [oauth] key = "parerga_und_paralipomena" -oauth.auth.max-attempts = 1 +[oauth.auth] +max-attempts = 1 [oauth.expiry] user-code = "1s" diff --git a/tests/src/jmap/auth_oauth.rs b/tests/src/jmap/auth_oauth.rs index c75ac660..6c56a665 100644 --- a/tests/src/jmap/auth_oauth.rs +++ b/tests/src/jmap/auth_oauth.rs @@ -345,7 +345,7 @@ async fn post_expect_redirect(url: &str, params: &AHashMap) -> S response .headers() .get(header::LOCATION) - .unwrap() + .expect("no Location header found in response") .to_str() .unwrap() .to_string() diff --git a/tests/src/jmap/crypto.rs b/tests/src/jmap/crypto.rs new file mode 100644 index 00000000..1616d56b --- /dev/null +++ b/tests/src/jmap/crypto.rs @@ -0,0 +1,308 @@ +/* + * Copyright (c) 2023 Stalwart Labs Ltd. + * + * This file is part of Stalwart Mail Server. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use std::{path::PathBuf, sync::Arc, time::Duration}; + +use ahash::AHashMap; +use jmap::{ + email::crypto::{ + try_parse_certs, Algorithm, EncryptMessage, EncryptionMethod, EncryptionParams, + }, + JMAP, +}; +use jmap_client::client::Client; +use jmap_proto::types::id::Id; +use mail_parser::{Message, MimeHeaders}; + +use crate::{directory::sql::create_test_user_with_email, jmap::delivery::SmtpConnection}; + +pub async fn test(server: Arc, client: &mut Client) { + println!("Running Encryption-at-rest tests..."); + + // Create test account + create_test_user_with_email( + server.directory.as_ref(), + "jdoe@example.com", + "12345", + "John Doe", + ) + .await; + let account_id = Id::from(server.get_account_id("jdoe@example.com").await.unwrap()).to_string(); + + // Update + let mut params = AHashMap::from_iter([ + ("email".to_string(), b"jdoe@example.com".to_vec()), + ("password".to_string(), b"12345".to_vec()), + ]); + + // Try importing using multiple methods and symmetric algos + for (file_name, method, num_certs) in [ + ("cert_smime.pem", EncryptionMethod::SMIME, 3), + ("cert_pgp.der", EncryptionMethod::PGP, 1), + ] { + params.insert( + "certificate".to_string(), + std::fs::read( + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("resources") + .join("crypto") + .join(file_name), + ) + .unwrap(), + ); + + for algo in [Algorithm::Aes128, Algorithm::Aes256] { + let encryption = format!( + "{}-{}", + match method { + EncryptionMethod::PGP => "pgp", + EncryptionMethod::SMIME => "smime", + }, + match algo { + Algorithm::Aes128 => "128", + Algorithm::Aes256 => "256", + } + ); + params.insert("encryption".to_string(), encryption.as_bytes().to_vec()); + let response = post(¶ms).await; + assert!( + response.contains(&format!("{num_certs} certificate")), + "got response {response}, expected {num_certs} certs" + ); + assert!( + response.contains(&format!("{method} ({algo})")), + "got response {response}, expected {encryption} algo" + ); + println!("response = {response}"); + } + } + + // Send a new message, which should be encrypted + let mut lmtp = SmtpConnection::connect().await; + lmtp.ingest( + "bill@example.com", + &["jdoe@example.com"], + concat!( + "From: bill@example.com\r\n", + "To: jdoe@example.com\r\n", + "Subject: TPS Report (should be encrypted)\r\n", + "\r\n", + "I'm going to need those TPS reports ASAP. ", + "So, if you could do that, that'd be great." + ), + ) + .await; + + // Send an encrypted message + lmtp.ingest( + "bill@example.com", + &["jdoe@example.com"], + concat!( + "From: bill@example.com\r\n", + "To: jdoe@example.com\r\n", + "Subject: TPS Report (already encrypted)\r\n", + "Content-Type: application/pkcs7-mime; name=\"smime.p7m\"; smime-type=enveloped-data\r\n", + "\r\n", + "xjMEZMYfNhYJKwYBBAHaRw8BAQdAYyTN1HzqapLw8xwkCGwa0OjsgT/JqhcB/+Dy", + "Ga1fsBrNG0pvaG4gRG9lIDxqb2huQGV4YW1wbGUub3JnPsKJBBMWCAAxFiEEg836", + "pwbXpuQ/THMtpJwd4oBfIrUFAmTGHzYCGwMECwkIBwUVCAkKCwUWAgMBAAAKCRCk", + "nB3igF8itYhyAQD2jEdeYa3gyQ47X9YWZTK1wEJkN8W9//V1fYl2XQwqlQEA0qBv", + "Ai6nUh99oDw+/zQ8DFIKdeb5Ti4tu/X58PdpiQ7OOARkxh82EgorBgEEAZdVAQUB", + "AQdAvXz2FbFN0DovQF/ACnZyczTsSIQp0mvmF1PE+aijbC8DAQgHwngEGBYIACAW", + "IQSDzfqnBtem5D9Mcy2knB3igF8itQUCZMYfNgIbDAAKCRCknB3igF8itRnoAQC3", + "GzPmgx7TnB+SexPuJV/DoKSMJ0/X+hbEFcZkulxaDQEAh+xiJCvf+ZNAKw6kFhsL", + "UuZhEDktxnY6Ehz3aB7FawA=", + "=KGrr", + ), + ) + .await; + + // Disable encryption + params.remove("certificate"); + params.insert("encryption".to_string(), "disable".as_bytes().to_vec()); + let response = post(¶ms).await; + assert!( + response.contains("Encryption at rest disabled"), + "got response {response}, expected 'Encryption at rest disabled'" + ); + + // Send a new message, which should NOT be encrypted + lmtp.ingest( + "bill@example.com", + &["jdoe@example.com"], + concat!( + "From: bill@example.com\r\n", + "To: jdoe@example.com\r\n", + "Subject: TPS Report (plain text)\r\n", + "\r\n", + "I'm going to need those TPS reports ASAP. ", + "So, if you could do that, that'd be great." + ), + ) + .await; + + // Check messages + client.set_default_account_id(&account_id); + let mut request = client.build(); + request.get_email(); + let emails = request.send_get_email().await.unwrap().take_list(); + assert_eq!(emails.len(), 3, "3 messages were expected: {:#?}.", emails); + + for email in emails { + let message = + String::from_utf8(client.download(email.blob_id().unwrap()).await.unwrap()).unwrap(); + if message.contains("should be encrypted") { + assert!( + message.contains("Content-Type: multipart/encrypted"), + "got message {message}, expected encrypted message" + ); + } else if message.contains("already encrypted") { + assert!( + message.contains("Content-Type: application/pkcs7-mime") + && message.contains("xjMEZMYfNhYJKwYBBAHaRw8BAQdAYy"), + "got message {message}, expected message to be left intact" + ); + } else if message.contains("plain text") { + assert!( + message.contains("I'm going to need those TPS reports ASAP."), + "got message {message}, expected plain text message" + ); + } else { + panic!("Unexpected message: {:#?}", message) + } + } +} + +#[tokio::test] +pub async fn import_certs_and_encrypt() { + for (name, expected_method, expected_certs) in [ + ("cert_pgp.pem", EncryptionMethod::PGP, 2), + ("cert_pgp.der", EncryptionMethod::PGP, 1), + ("cert_smime.pem", EncryptionMethod::SMIME, 3), + ("cert_smime.der", EncryptionMethod::SMIME, 1), + ] { + let (method, mut certs) = try_parse_certs( + std::fs::read( + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("resources") + .join("crypto") + .join(name), + ) + .unwrap(), + ) + .unwrap(); + + assert_eq!(method, expected_method); + assert_eq!(certs.len(), expected_certs); + + if method == EncryptionMethod::PGP && certs.len() == 2 { + // PGP library won't encrypt using EC + certs.pop(); + } + + let mut params = EncryptionParams { + method, + algo: Algorithm::Aes128, + certs, + }; + + for algo in [Algorithm::Aes128, Algorithm::Aes256] { + let message = Message::parse(b"Subject: test\r\ntest\r\n").unwrap(); + assert!(!message.is_encrypted()); + params.algo = algo; + message.encrypt(¶ms).await.unwrap(); + } + } + + // S/MIME and PGP should not be allowed mixed + assert!(try_parse_certs( + std::fs::read( + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("resources") + .join("crypto") + .join("cert_mixed.pem"), + ) + .unwrap(), + ) + .is_err()); +} + +#[test] +pub fn check_is_encrypted() { + let messages = std::fs::read_to_string( + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("resources") + .join("crypto") + .join("is_encrypted.txt"), + ) + .unwrap(); + + for raw_message in messages.split("---") { + let is_encrypted = raw_message.contains("TRUE"); + let message = Message::parse(raw_message.trim().as_bytes()).unwrap(); + assert!(message.content_type().is_some()); + assert_eq!( + message.is_encrypted(), + is_encrypted, + "failed for {raw_message}" + ); + } +} + +async fn post(params: &AHashMap>) -> String { + let mut form = reqwest::multipart::Form::new(); + for (key, value) in params { + form = if key != "certificate" { + form.text( + key.to_string(), + std::str::from_utf8(value).unwrap().to_string(), + ) + } else { + form.part( + key.to_string(), + reqwest::multipart::Part::bytes(value.to_vec()) + .file_name("certificate") + .mime_str("application/octet-stream") + .unwrap(), + ) + } + } + + String::from_utf8( + reqwest::Client::builder() + .timeout(Duration::from_millis(500)) + .danger_accept_invalid_certs(true) + .build() + .unwrap_or_default() + .post("https://127.0.0.1:8899/crypto") + .multipart(form) + .send() + .await + .unwrap() + .bytes() + .await + .unwrap() + .to_vec(), + ) + .unwrap() +} diff --git a/tests/src/jmap/mod.rs b/tests/src/jmap/mod.rs index 5562ecd3..338ffdbb 100644 --- a/tests/src/jmap/mod.rs +++ b/tests/src/jmap/mod.rs @@ -40,6 +40,7 @@ use crate::{ pub mod auth_acl; pub mod auth_limits; pub mod auth_oauth; +pub mod crypto; pub mod delivery; pub mod email_changes; pub mod email_copy; @@ -205,7 +206,9 @@ remote-domains = ["remote.org", "foobar.com", "test.com", "other_domain.com"] [oauth] key = "parerga_und_paralipomena" -oauth.auth.max-attempts = 1 + +[oauth.auth] +max-attempts = 1 [oauth.expiry] user-code = "1s" @@ -247,6 +250,7 @@ pub async fn jmap_tests() { email_submission::test(params.server.clone(), &mut params.client).await; websocket::test(params.server.clone(), &mut params.client).await; quota::test(params.server.clone(), &mut params.client).await; + crypto::test(params.server.clone(), &mut params.client).await; if delete { params.temp_dir.delete();