mirror of
https://github.com/stalwartlabs/mail-server.git
synced 2024-11-10 09:32:19 +08:00
Replaced rpgp with sequoia-pgp
This commit is contained in:
parent
833db92ded
commit
cb41c91fb4
4 changed files with 435 additions and 460 deletions
717
Cargo.lock
generated
717
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -42,7 +42,7 @@ chrono = "0.4"
|
|||
dashmap = "5.4"
|
||||
aes = "0.8.3"
|
||||
cbc = { version = "0.1.2", features = ["alloc"] }
|
||||
pgp = "0.10.2"
|
||||
sequoia-openpgp = "1.16"
|
||||
rand = "0.8.5"
|
||||
rasn = "0.9.5"
|
||||
rasn-cms = "0.9.5"
|
||||
|
|
|
@ -21,13 +21,22 @@
|
|||
* for more details.
|
||||
*/
|
||||
|
||||
use std::{borrow::Cow, collections::BTreeSet, fmt::Display};
|
||||
use std::{borrow::Cow, collections::BTreeSet, fmt::Display, io::Cursor};
|
||||
|
||||
use crate::{
|
||||
api::{http::ToHttpResponse, HtmlResponse, HttpRequest, HttpResponse},
|
||||
auth::{oauth::FormData, rate_limit::RemoteAddress},
|
||||
JMAP,
|
||||
};
|
||||
use aes::cipher::{block_padding::Pkcs7, BlockEncryptMut, KeyIvInit};
|
||||
use jmap_proto::types::{collection::Collection, property::Property};
|
||||
use mail_builder::{encoders::base64::base64_encode_mime, mime::make_boundary};
|
||||
use mail_parser::{decoders::base64::base64_decode, Message, MessageParser, MimeHeaders};
|
||||
use pgp::{composed, crypto::sym::SymmetricKeyAlgorithm, Deserializable, SignedPublicKey};
|
||||
use openpgp::{
|
||||
parse::Parse,
|
||||
serialize::stream,
|
||||
types::{KeyFlags, SymmetricAlgorithm},
|
||||
};
|
||||
use rand::{rngs::StdRng, RngCore, SeedableRng};
|
||||
use rasn::types::{ObjectIdentifier, OctetString};
|
||||
use rasn_cms::{
|
||||
|
@ -38,17 +47,12 @@ use rasn_cms::{
|
|||
CONTENT_ENVELOPED_DATA,
|
||||
};
|
||||
use rsa::{pkcs1::DecodeRsaPublicKey, Pkcs1v15Encrypt, RsaPublicKey};
|
||||
use sequoia_openpgp as openpgp;
|
||||
use store::{
|
||||
write::{BatchBuilder, ToBitmaps, F_CLEAR, F_VALUE},
|
||||
Deserialize, Serialize,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
api::{http::ToHttpResponse, HtmlResponse, HttpRequest, HttpResponse},
|
||||
auth::{oauth::FormData, rate_limit::RemoteAddress},
|
||||
JMAP,
|
||||
};
|
||||
|
||||
const CRYPT_HTML_HEADER: &str = include_str!("../../../../resources/htx/crypto_header.htx");
|
||||
const CRYPT_HTML_FOOTER: &str = include_str!("../../../../resources/htx/crypto_footer.htx");
|
||||
const CRYPT_HTML_FORM: &str = include_str!("../../../../resources/htx/crypto_form.htx");
|
||||
|
@ -56,6 +60,8 @@ const CRYPT_HTML_SUCCESS: &str = include_str!("../../../../resources/htx/crypto_
|
|||
const CRYPT_HTML_DISABLED: &str = include_str!("../../../../resources/htx/crypto_disabled.htx");
|
||||
const CRYPT_HTML_ERROR: &str = include_str!("../../../../resources/htx/crypto_error.htx");
|
||||
|
||||
const P: openpgp::policy::StandardPolicy<'static> = openpgp::policy::StandardPolicy::new();
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum EncryptMessageError {
|
||||
AlreadyEncrypted,
|
||||
|
@ -132,7 +138,7 @@ impl EncryptMessage for Message<'_> {
|
|||
outer_message.extend_from_slice(boundary.as_bytes());
|
||||
outer_message.extend_from_slice(
|
||||
concat!(
|
||||
"\r\nContent-Type: application/pgp-encrypted\r\n",
|
||||
"\r\nContent-Type: application/pgp-encrypted\r\n\r\n",
|
||||
"Version: 1\r\n\r\n--"
|
||||
)
|
||||
.as_bytes(),
|
||||
|
@ -146,42 +152,85 @@ impl EncryptMessage for Message<'_> {
|
|||
.as_bytes(),
|
||||
);
|
||||
|
||||
// Parse public key
|
||||
let mut keys = Vec::with_capacity(params.certs.len());
|
||||
for cert in ¶ms.certs {
|
||||
keys.push(SignedPublicKey::from_bytes(&cert[..]).map_err(|err| {
|
||||
let certs = params
|
||||
.certs
|
||||
.iter()
|
||||
.map(openpgp::Cert::from_bytes)
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|err| {
|
||||
EncryptMessageError::Error(format!(
|
||||
"Failed to parse OpenPGP public key: {}",
|
||||
err
|
||||
))
|
||||
})?);
|
||||
}
|
||||
})?;
|
||||
|
||||
// Encrypt contents (TODO: use rayon)
|
||||
let algo = params.algo;
|
||||
let encrypted_contents = tokio::task::spawn_blocking(move || {
|
||||
composed::message::Message::new_literal_bytes("none", &inner_message)
|
||||
.encrypt_to_keys(
|
||||
&mut StdRng::from_entropy(),
|
||||
match algo {
|
||||
Algorithm::Aes128 => SymmetricKeyAlgorithm::AES128,
|
||||
Algorithm::Aes256 => SymmetricKeyAlgorithm::AES256,
|
||||
},
|
||||
&keys.iter().collect::<Vec<_>>(),
|
||||
)
|
||||
// Parse public key
|
||||
let mut keys = Vec::with_capacity(certs.len());
|
||||
let policy = openpgp::policy::StandardPolicy::new();
|
||||
|
||||
for cert in &certs {
|
||||
for key in cert
|
||||
.keys()
|
||||
.with_policy(&policy, None)
|
||||
.supported()
|
||||
.alive()
|
||||
.revoked(false)
|
||||
.key_flags(&KeyFlags::empty().set_transport_encryption())
|
||||
{
|
||||
keys.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
// Compose a writer stack corresponding to the output format and
|
||||
// packet structure we want.
|
||||
let mut sink = Vec::with_capacity(inner_message.len());
|
||||
|
||||
// Stream an OpenPGP message.
|
||||
let message = stream::Armorer::new(stream::Message::new(&mut sink))
|
||||
.build()
|
||||
.map_err(|err| {
|
||||
EncryptMessageError::Error(format!("Failed to create armorer: {}", err))
|
||||
})?;
|
||||
let message = stream::Encryptor::for_recipients(message, keys)
|
||||
.symmetric_algo(match algo {
|
||||
Algorithm::Aes128 => SymmetricAlgorithm::AES128,
|
||||
Algorithm::Aes256 => SymmetricAlgorithm::AES256,
|
||||
})
|
||||
.build()
|
||||
.map_err(|err| {
|
||||
EncryptMessageError::Error(format!(
|
||||
"Failed to build encryptor: {}",
|
||||
err
|
||||
))
|
||||
})?;
|
||||
let mut message =
|
||||
stream::LiteralWriter::new(message).build().map_err(|err| {
|
||||
EncryptMessageError::Error(format!(
|
||||
"Failed to create literal writer: {}",
|
||||
err
|
||||
))
|
||||
})?;
|
||||
std::io::copy(&mut Cursor::new(inner_message), &mut message).map_err(
|
||||
|err| {
|
||||
EncryptMessageError::Error(format!(
|
||||
"Failed to encrypt message: {}",
|
||||
err
|
||||
))
|
||||
})?
|
||||
.to_armored_string(None)
|
||||
.map_err(|err| {
|
||||
EncryptMessageError::Error(format!(
|
||||
"Failed to convert to armored string: {}",
|
||||
err
|
||||
))
|
||||
})
|
||||
},
|
||||
)?;
|
||||
message.finalize().map_err(|err| {
|
||||
EncryptMessageError::Error(format!("Failed to finalize message: {}", err))
|
||||
})?;
|
||||
|
||||
String::from_utf8(sink).map_err(|err| {
|
||||
EncryptMessageError::Error(format!(
|
||||
"Failed to convert encrypted message to UTF-8: {}",
|
||||
err
|
||||
))
|
||||
})
|
||||
})
|
||||
.await
|
||||
.map_err(|err| {
|
||||
|
@ -382,26 +431,43 @@ pub fn try_parse_certs(bytes: Vec<u8>) -> Result<(EncryptionMethod, Vec<Vec<u8>>
|
|||
Ok(result)
|
||||
} else if rasn::der::decode::<rasn_pkix::Certificate>(&bytes[..]).is_ok() {
|
||||
Ok((EncryptionMethod::SMIME, vec![bytes]))
|
||||
} else if SignedPublicKey::from_bytes(&bytes[..]).is_ok() {
|
||||
Ok((EncryptionMethod::PGP, vec![bytes]))
|
||||
} else if let Ok(cert) = openpgp::Cert::from_bytes(&bytes[..]) {
|
||||
if !has_pgp_keys(cert) {
|
||||
Ok((EncryptionMethod::PGP, vec![bytes]))
|
||||
} else {
|
||||
Err("Could not find any suitable keys in certificate".to_string())
|
||||
}
|
||||
} else {
|
||||
Err("Could not find any valid certificates".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn has_pgp_keys(cert: openpgp::Cert) -> bool {
|
||||
cert.keys()
|
||||
.with_policy(&P, None)
|
||||
.supported()
|
||||
.alive()
|
||||
.revoked(false)
|
||||
.key_flags(&KeyFlags::empty().set_transport_encryption())
|
||||
.next()
|
||||
.is_some()
|
||||
}
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn try_parse_pem(bytes: &[u8]) -> Result<Option<(EncryptionMethod, Vec<Vec<u8>>)>, String> {
|
||||
let mut bytes = bytes.iter();
|
||||
fn try_parse_pem(bytes_: &[u8]) -> Result<Option<(EncryptionMethod, Vec<Vec<u8>>)>, String> {
|
||||
let mut bytes = bytes_.iter().enumerate();
|
||||
let mut buf = vec![];
|
||||
let mut method = None;
|
||||
let mut certs = vec![];
|
||||
|
||||
loop {
|
||||
// Find start of PEM block
|
||||
for &ch in bytes.by_ref() {
|
||||
let mut start_pos = 0;
|
||||
for (pos, &ch) in bytes.by_ref() {
|
||||
if ch.is_ascii_whitespace() {
|
||||
continue;
|
||||
} else if ch == b'-' {
|
||||
start_pos = pos;
|
||||
break;
|
||||
} else {
|
||||
return Ok(None);
|
||||
|
@ -409,7 +475,7 @@ fn try_parse_pem(bytes: &[u8]) -> Result<Option<(EncryptionMethod, Vec<Vec<u8>>)
|
|||
}
|
||||
|
||||
// Find block type
|
||||
for &ch in bytes.by_ref() {
|
||||
for (_, &ch) in bytes.by_ref() {
|
||||
match ch {
|
||||
b'-' => (),
|
||||
b'\n' => break,
|
||||
|
@ -443,7 +509,7 @@ fn try_parse_pem(bytes: &[u8]) -> Result<Option<(EncryptionMethod, Vec<Vec<u8>>)
|
|||
} else {
|
||||
// Ignore block
|
||||
let mut found_end = false;
|
||||
for &ch in bytes.by_ref() {
|
||||
for (_, &ch) in bytes.by_ref() {
|
||||
if ch == b'-' {
|
||||
found_end = true;
|
||||
} else if ch == b'\n' && found_end {
|
||||
|
@ -457,13 +523,15 @@ fn try_parse_pem(bytes: &[u8]) -> Result<Option<(EncryptionMethod, Vec<Vec<u8>>)
|
|||
// Collect base64
|
||||
buf.clear();
|
||||
let mut found_end = false;
|
||||
for &ch in bytes.by_ref() {
|
||||
let mut end_pos = 0;
|
||||
for (pos, &ch) in bytes.by_ref() {
|
||||
match ch {
|
||||
b'-' => {
|
||||
found_end = true;
|
||||
}
|
||||
b'\n' => {
|
||||
if found_end {
|
||||
end_pos = pos;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -479,18 +547,29 @@ fn try_parse_pem(bytes: &[u8]) -> Result<Option<(EncryptionMethod, Vec<Vec<u8>>)
|
|||
let cert = base64_decode(&buf)
|
||||
.ok_or_else(|| "Failed to decode base64 certificate.".to_string())?;
|
||||
match method.unwrap() {
|
||||
EncryptionMethod::PGP => {
|
||||
if let Err(err) = SignedPublicKey::from_bytes(&cert[..]) {
|
||||
return Err(format!("Failed to decode OpenPGP public key: {}", err));
|
||||
EncryptionMethod::PGP => match openpgp::Cert::from_bytes(bytes_) {
|
||||
Ok(cert) => {
|
||||
if !has_pgp_keys(cert) {
|
||||
return Err(
|
||||
"Could not find any suitable keys in OpenPGP public key".to_string()
|
||||
);
|
||||
}
|
||||
certs.push(
|
||||
bytes_
|
||||
.get(start_pos..end_pos + 1)
|
||||
.unwrap_or_default()
|
||||
.to_vec(),
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(err) => return Err(format!("Failed to decode OpenPGP public key: {}", err)),
|
||||
},
|
||||
EncryptionMethod::SMIME => {
|
||||
if let Err(err) = rasn::der::decode::<rasn_pkix::Certificate>(&cert) {
|
||||
return Err(format!("Failed to decode X509 certificate: {}", err));
|
||||
}
|
||||
certs.push(cert);
|
||||
}
|
||||
}
|
||||
certs.push(cert);
|
||||
buf.clear();
|
||||
}
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ protocol = "jmap"
|
|||
max-connections = 8192
|
||||
|
||||
[server.listener.imap]
|
||||
bind = ["0.0.0.0:143"]
|
||||
bind = ["0.0.0.0:9991"]
|
||||
protocol = "imap"
|
||||
max-connections = 8192
|
||||
|
||||
|
@ -31,7 +31,7 @@ protocol = 'smtp'
|
|||
tls.implicit = true
|
||||
|
||||
[server.listener.smtp]
|
||||
bind = ['0.0.0.0:587']
|
||||
bind = ['0.0.0.0:9995']
|
||||
greeting = 'Test SMTP instance'
|
||||
protocol = 'smtp'
|
||||
tls.implicit = false
|
||||
|
@ -46,7 +46,8 @@ certificate = "default"
|
|||
|
||||
[global.tracing]
|
||||
method = "stdout"
|
||||
level = "trace"
|
||||
#level = "trace"
|
||||
level = "info"
|
||||
|
||||
[session.ehlo]
|
||||
reject-non-fqdn = false
|
||||
|
|
Loading…
Reference in a new issue