mirror of
https://github.com/stalwartlabs/mail-server.git
synced 2025-10-18 00:15:49 +08:00
769 lines
30 KiB
Rust
769 lines
30 KiB
Rust
/*
|
|
* 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 <http://www.gnu.org/licenses/>.
|
|
*
|
|
* 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::{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 openpgp::{
|
|
parse::Parse,
|
|
serialize::stream,
|
|
types::{KeyFlags, SymmetricAlgorithm},
|
|
};
|
|
use rand::{rngs::StdRng, RngCore, SeedableRng};
|
|
use rasn::types::{ObjectIdentifier, OctetString};
|
|
use rasn_cms::{
|
|
algorithms::{AES128_CBC, AES256_CBC, RSA},
|
|
pkcs7_compat::EncapsulatedContentInfo,
|
|
AlgorithmIdentifier, EncryptedContent, EncryptedContentInfo, EncryptedKey, EnvelopedData,
|
|
IssuerAndSerialNumber, KeyTransRecipientInfo, RecipientIdentifier, RecipientInfo, CONTENT_DATA,
|
|
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,
|
|
};
|
|
|
|
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");
|
|
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");
|
|
|
|
const P: openpgp::policy::StandardPolicy<'static> = openpgp::policy::StandardPolicy::new();
|
|
|
|
#[derive(Debug)]
|
|
pub enum EncryptMessageError {
|
|
AlreadyEncrypted,
|
|
Error(String),
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
|
|
pub enum Algorithm {
|
|
Aes128,
|
|
Aes256,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
|
pub enum EncryptionMethod {
|
|
PGP,
|
|
SMIME,
|
|
}
|
|
|
|
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
|
pub struct EncryptionParams {
|
|
pub method: EncryptionMethod,
|
|
pub algo: Algorithm,
|
|
pub certs: Vec<Vec<u8>>,
|
|
}
|
|
|
|
#[allow(async_fn_in_trait)]
|
|
pub trait EncryptMessage {
|
|
async fn encrypt(&self, params: &EncryptionParams) -> Result<Vec<u8>, EncryptMessageError>;
|
|
fn is_encrypted(&self) -> bool;
|
|
}
|
|
|
|
impl EncryptMessage for Message<'_> {
|
|
async fn encrypt(&self, params: &EncryptionParams) -> Result<Vec<u8>, EncryptMessageError> {
|
|
let root = self.root_part();
|
|
let raw_message = self.raw_message();
|
|
let mut outer_message = Vec::with_capacity((raw_message.len() as f64 * 1.5) as usize);
|
|
let mut inner_message = Vec::with_capacity(raw_message.len());
|
|
|
|
// Move MIME headers and body to inner message
|
|
for header in root.headers() {
|
|
(if header.name.is_mime_header() {
|
|
&mut inner_message
|
|
} else {
|
|
&mut outer_message
|
|
})
|
|
.extend_from_slice(&raw_message[header.offset_field()..header.offset_end()]);
|
|
}
|
|
inner_message.extend_from_slice(b"\r\n");
|
|
inner_message.extend_from_slice(&raw_message[root.raw_body_offset()..]);
|
|
|
|
// Encrypt inner message
|
|
match params.method {
|
|
EncryptionMethod::PGP => {
|
|
// Prepare encrypted message
|
|
let boundary = make_boundary("_");
|
|
outer_message.extend_from_slice(
|
|
concat!(
|
|
"Content-Type: multipart/encrypted;\r\n\t",
|
|
"protocol=\"application/pgp-encrypted\";\r\n\t",
|
|
"boundary=\""
|
|
)
|
|
.as_bytes(),
|
|
);
|
|
outer_message.extend_from_slice(boundary.as_bytes());
|
|
outer_message.extend_from_slice(
|
|
concat!(
|
|
"\"\r\n\r\n",
|
|
"OpenPGP/MIME message (Automatically encrypted by Stalwart)\r\n\r\n",
|
|
"--"
|
|
)
|
|
.as_bytes(),
|
|
);
|
|
outer_message.extend_from_slice(boundary.as_bytes());
|
|
outer_message.extend_from_slice(
|
|
concat!(
|
|
"\r\nContent-Type: application/pgp-encrypted\r\n\r\n",
|
|
"Version: 1\r\n\r\n--"
|
|
)
|
|
.as_bytes(),
|
|
);
|
|
outer_message.extend_from_slice(boundary.as_bytes());
|
|
outer_message.extend_from_slice(
|
|
concat!(
|
|
"\r\nContent-Type: application/octet-stream; name=\"encrypted.asc\"\r\n",
|
|
"Content-Disposition: inline; filename=\"encrypted.asc\"\r\n\r\n"
|
|
)
|
|
.as_bytes(),
|
|
);
|
|
|
|
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 || {
|
|
// 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::Encryptor2::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
|
|
))
|
|
},
|
|
)?;
|
|
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| {
|
|
EncryptMessageError::Error(format!("Failed to encrypt message: {}", err))
|
|
})??;
|
|
outer_message.extend_from_slice(encrypted_contents.as_bytes());
|
|
outer_message.extend_from_slice(b"\r\n--");
|
|
outer_message.extend_from_slice(boundary.as_bytes());
|
|
outer_message.extend_from_slice(b"--\r\n");
|
|
}
|
|
EncryptionMethod::SMIME => {
|
|
// Generate random IV
|
|
let mut rng = StdRng::from_entropy();
|
|
let mut iv = vec![0u8; 16];
|
|
rng.fill_bytes(&mut iv);
|
|
|
|
// Generate random key
|
|
let mut key = vec![0u8; params.algo.key_size()];
|
|
rng.fill_bytes(&mut key);
|
|
|
|
// Encrypt contents (TODO: use rayon)
|
|
let algo = params.algo;
|
|
let (encrypted_contents, key, iv) = tokio::task::spawn_blocking(move || {
|
|
(algo.encrypt(&key, &iv, &inner_message), key, iv)
|
|
})
|
|
.await
|
|
.map_err(|err| {
|
|
EncryptMessageError::Error(format!("Failed to encrypt message: {}", err))
|
|
})?;
|
|
|
|
// Encrypt key using public keys
|
|
#[allow(clippy::mutable_key_type)]
|
|
let mut recipient_infos = BTreeSet::new();
|
|
for cert in ¶ms.certs {
|
|
let cert =
|
|
rasn::der::decode::<rasn_pkix::Certificate>(cert).map_err(|err| {
|
|
EncryptMessageError::Error(format!(
|
|
"Failed to parse certificate: {}",
|
|
err
|
|
))
|
|
})?;
|
|
|
|
let public_key = RsaPublicKey::from_pkcs1_der(
|
|
cert.tbs_certificate
|
|
.subject_public_key_info
|
|
.subject_public_key
|
|
.as_raw_slice(),
|
|
)
|
|
.map_err(|err| {
|
|
EncryptMessageError::Error(format!("Failed to parse public key: {}", err))
|
|
})?;
|
|
let encrypted_key = public_key
|
|
.encrypt(&mut rng, Pkcs1v15Encrypt, &key[..])
|
|
.map_err(|err| {
|
|
EncryptMessageError::Error(format!("Failed to encrypt key: {}", err))
|
|
})
|
|
.unwrap();
|
|
|
|
recipient_infos.insert(RecipientInfo::KeyTransRecipientInfo(
|
|
KeyTransRecipientInfo {
|
|
version: 0.into(),
|
|
rid: RecipientIdentifier::IssuerAndSerialNumber(
|
|
IssuerAndSerialNumber {
|
|
issuer: cert.tbs_certificate.issuer,
|
|
serial_number: cert.tbs_certificate.serial_number,
|
|
},
|
|
),
|
|
key_encryption_algorithm: AlgorithmIdentifier {
|
|
algorithm: RSA.into(),
|
|
parameters: Some(
|
|
rasn::der::encode(&())
|
|
.map_err(|err| {
|
|
EncryptMessageError::Error(format!(
|
|
"Failed to encode RSA algorithm identifier: {}",
|
|
err
|
|
))
|
|
})?
|
|
.into(),
|
|
),
|
|
},
|
|
encrypted_key: EncryptedKey::from(encrypted_key),
|
|
},
|
|
));
|
|
}
|
|
|
|
let pkcs7 = rasn::der::encode(&EncapsulatedContentInfo {
|
|
content_type: CONTENT_ENVELOPED_DATA.into(),
|
|
content: Some(
|
|
rasn::der::encode(&EnvelopedData {
|
|
version: 0.into(),
|
|
originator_info: None,
|
|
recipient_infos,
|
|
encrypted_content_info: EncryptedContentInfo {
|
|
content_type: CONTENT_DATA.into(),
|
|
content_encryption_algorithm: AlgorithmIdentifier {
|
|
algorithm: params.algo.to_algorithm_identifier(),
|
|
parameters: Some(
|
|
rasn::der::encode(&OctetString::from(iv))
|
|
.map_err(|err| {
|
|
EncryptMessageError::Error(format!(
|
|
"Failed to encode IV: {}",
|
|
err
|
|
))
|
|
})?
|
|
.into(),
|
|
),
|
|
},
|
|
encrypted_content: Some(EncryptedContent::from(encrypted_contents)),
|
|
},
|
|
unprotected_attrs: None,
|
|
})
|
|
.map_err(|err| {
|
|
EncryptMessageError::Error(format!(
|
|
"Failed to encode EnvelopedData: {}",
|
|
err
|
|
))
|
|
})?
|
|
.into(),
|
|
),
|
|
})
|
|
.map_err(|err| {
|
|
EncryptMessageError::Error(format!("Failed to encode ContentInfo: {}", err))
|
|
})?;
|
|
|
|
// Generate message
|
|
outer_message.extend_from_slice(
|
|
concat!(
|
|
"Content-Type: application/pkcs7-mime;\r\n",
|
|
"\tname=\"smime.p7m\";\r\n",
|
|
"\tsmime-type=enveloped-data\r\n",
|
|
"Content-Disposition: attachment;\r\n",
|
|
"\tfilename=\"smime.p7m\"\r\n",
|
|
"Content-Transfer-Encoding: base64\r\n\r\n"
|
|
)
|
|
.as_bytes(),
|
|
);
|
|
base64_encode_mime(&pkcs7, &mut outer_message, false).map_err(|err| {
|
|
EncryptMessageError::Error(format!("Failed to base64 encode PKCS7: {}", err))
|
|
})?;
|
|
}
|
|
}
|
|
|
|
Ok(outer_message)
|
|
}
|
|
|
|
fn is_encrypted(&self) -> bool {
|
|
self.content_type().map_or(false, |ct| {
|
|
let main_type = ct.c_type.as_ref();
|
|
let sub_type = ct
|
|
.c_subtype
|
|
.as_ref()
|
|
.map(|s| s.as_ref())
|
|
.unwrap_or_default();
|
|
|
|
(main_type.eq_ignore_ascii_case("application")
|
|
&& (sub_type.eq_ignore_ascii_case("pkcs7-mime")
|
|
|| sub_type.eq_ignore_ascii_case("pkcs7-signature")
|
|
|| (sub_type.eq_ignore_ascii_case("octet-stream")
|
|
&& self.attachment_name().map_or(false, |name| {
|
|
name.rsplit_once('.').map_or(false, |(_, ext)| {
|
|
["p7m", "p7s", "p7c", "p7z"].contains(&ext)
|
|
})
|
|
}))))
|
|
|| (main_type.eq_ignore_ascii_case("multipart")
|
|
&& sub_type.eq_ignore_ascii_case("encrypted"))
|
|
})
|
|
}
|
|
}
|
|
|
|
impl Algorithm {
|
|
fn key_size(&self) -> usize {
|
|
match self {
|
|
Algorithm::Aes128 => 16,
|
|
Algorithm::Aes256 => 32,
|
|
}
|
|
}
|
|
|
|
fn to_algorithm_identifier(self) -> ObjectIdentifier {
|
|
match self {
|
|
Algorithm::Aes128 => AES128_CBC.into(),
|
|
Algorithm::Aes256 => AES256_CBC.into(),
|
|
}
|
|
}
|
|
|
|
fn encrypt(&self, key: &[u8], iv: &[u8], contents: &[u8]) -> Vec<u8> {
|
|
match self {
|
|
Algorithm::Aes128 => cbc::Encryptor::<aes::Aes128>::new(key.into(), iv.into())
|
|
.encrypt_padded_vec_mut::<Pkcs7>(contents),
|
|
Algorithm::Aes256 => cbc::Encryptor::<aes::Aes256>::new(key.into(), iv.into())
|
|
.encrypt_padded_vec_mut::<Pkcs7>(contents),
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn try_parse_certs(bytes: Vec<u8>) -> Result<(EncryptionMethod, Vec<Vec<u8>>), String> {
|
|
// Check if it's a PEM file
|
|
if let Some(result) = try_parse_pem(&bytes)? {
|
|
Ok(result)
|
|
} else if rasn::der::decode::<rasn_pkix::Certificate>(&bytes[..]).is_ok() {
|
|
Ok((EncryptionMethod::SMIME, 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().enumerate();
|
|
let mut buf = vec![];
|
|
let mut method = None;
|
|
let mut certs = vec![];
|
|
|
|
loop {
|
|
// Find start of PEM block
|
|
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);
|
|
}
|
|
}
|
|
|
|
// Find block type
|
|
for (_, &ch) in bytes.by_ref() {
|
|
match ch {
|
|
b'-' => (),
|
|
b'\n' => break,
|
|
_ => {
|
|
if ch.is_ascii() {
|
|
buf.push(ch.to_ascii_uppercase());
|
|
} else {
|
|
return Ok(None);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if buf.is_empty() {
|
|
break;
|
|
}
|
|
|
|
// Find type
|
|
let tag = std::str::from_utf8(&buf).unwrap();
|
|
if tag.contains("CERTIFICATE") {
|
|
if method.map_or(false, |m| m == EncryptionMethod::PGP) {
|
|
return Err("Cannot mix OpenPGP and S/MIME certificates".to_string());
|
|
} else {
|
|
method = Some(EncryptionMethod::SMIME);
|
|
}
|
|
} else if tag.contains("PGP") {
|
|
if method.map_or(false, |m| m == EncryptionMethod::SMIME) {
|
|
return Err("Cannot mix OpenPGP and S/MIME certificates".to_string());
|
|
} else {
|
|
method = Some(EncryptionMethod::PGP);
|
|
}
|
|
} else {
|
|
// Ignore block
|
|
let mut found_end = false;
|
|
for (_, &ch) in bytes.by_ref() {
|
|
if ch == b'-' {
|
|
found_end = true;
|
|
} else if ch == b'\n' && found_end {
|
|
break;
|
|
}
|
|
}
|
|
buf.clear();
|
|
continue;
|
|
}
|
|
|
|
// Collect base64
|
|
buf.clear();
|
|
let mut found_end = false;
|
|
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;
|
|
}
|
|
}
|
|
_ => {
|
|
if !ch.is_ascii_whitespace() {
|
|
buf.push(ch);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Decode base64
|
|
let cert = base64_decode(&buf)
|
|
.ok_or_else(|| "Failed to decode base64 certificate.".to_string())?;
|
|
match method.unwrap() {
|
|
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);
|
|
}
|
|
}
|
|
buf.clear();
|
|
}
|
|
|
|
Ok(method.map(|method| (method, certs)))
|
|
}
|
|
|
|
impl Serialize for &EncryptionParams {
|
|
fn serialize(self) -> Vec<u8> {
|
|
let len = bincode::serialized_size(&self).unwrap_or_default();
|
|
let mut buf = Vec::with_capacity(len as usize + 1);
|
|
buf.push(1);
|
|
let _ = bincode::serialize_into(&mut buf, &self);
|
|
buf
|
|
}
|
|
}
|
|
|
|
impl Deserialize for EncryptionParams {
|
|
fn deserialize(bytes: &[u8]) -> store::Result<Self> {
|
|
let version = *bytes.first().ok_or_else(|| {
|
|
store::Error::InternalError(
|
|
"Failed to read version while deserializing encryption params".to_string(),
|
|
)
|
|
})?;
|
|
match version {
|
|
1 if bytes.len() > 1 => bincode::deserialize(&bytes[1..]).map_err(|err| {
|
|
store::Error::InternalError(format!(
|
|
"Failed to deserialize encryption params: {}",
|
|
err
|
|
))
|
|
}),
|
|
|
|
_ => Err(store::Error::InternalError(format!(
|
|
"Unknown encryption params version: {}",
|
|
version
|
|
))),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ToBitmaps for &EncryptionParams {
|
|
fn to_bitmaps(&self, _: &mut Vec<store::write::Operation>, _: u8, _: bool) {
|
|
unreachable!()
|
|
}
|
|
}
|
|
|
|
impl JMAP {
|
|
// Code authorization flow, handles an authorization request
|
|
pub async fn handle_crypto_update(
|
|
&self,
|
|
req: &mut HttpRequest,
|
|
remote_addr: &RemoteAddress,
|
|
) -> HttpResponse {
|
|
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 {
|
|
Ok(form) => form,
|
|
Err(err) => return err,
|
|
};
|
|
|
|
match self.validate_form(form, remote_addr).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 => {
|
|
response.push_str(CRYPT_HTML_FORM);
|
|
}
|
|
_ => unreachable!(),
|
|
};
|
|
|
|
response.push_str(CRYPT_HTML_FOOTER);
|
|
|
|
HtmlResponse::new(response).into_http_response()
|
|
}
|
|
|
|
async fn validate_form(
|
|
&self,
|
|
mut form: FormData,
|
|
remote_addr: &RemoteAddress,
|
|
) -> Result<Option<EncryptionParams>, Cow<str>> {
|
|
let certificate = form.remove_bytes("certificate");
|
|
if let (Some(email), Some(password), Some(encryption)) = (
|
|
form.get("email"),
|
|
form.get("password"),
|
|
form.get("encryption"),
|
|
) {
|
|
// Validate fields
|
|
if email.is_empty() || password.is_empty() {
|
|
return Err(Cow::from("Please enter your login and password"));
|
|
} else if encryption != "disable" && certificate.as_ref().map_or(true, |c| c.is_empty())
|
|
{
|
|
return Err(Cow::from("Please select one or more certificates"));
|
|
}
|
|
|
|
// Authenticate
|
|
let token = self
|
|
.authenticate_plain(email, password, remote_addr)
|
|
.await
|
|
.ok_or_else(|| Cow::from("Invalid login or password"))?;
|
|
if encryption != "disable" {
|
|
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,
|
|
("smime-256", EncryptionMethod::SMIME) => Algorithm::Aes256,
|
|
("smime-128", EncryptionMethod::SMIME) => Algorithm::Aes128,
|
|
_ => {
|
|
return Err(Cow::from(
|
|
"No valid certificates found for the selected encryption",
|
|
));
|
|
}
|
|
};
|
|
let params = EncryptionParams {
|
|
method,
|
|
algo,
|
|
certs,
|
|
};
|
|
|
|
// Try a test encryption
|
|
if let Err(EncryptMessageError::Error(message)) = MessageParser::new()
|
|
.parse("Subject: test\r\ntest\r\n".as_bytes())
|
|
.unwrap()
|
|
.encrypt(¶ms)
|
|
.await
|
|
{
|
|
return Err(Cow::from(message));
|
|
}
|
|
|
|
// Save encryption params
|
|
let mut batch = BatchBuilder::new();
|
|
batch
|
|
.with_account_id(token.primary_id())
|
|
.with_collection(Collection::Principal)
|
|
.update_document(0)
|
|
.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();
|
|
batch
|
|
.with_account_id(token.primary_id())
|
|
.with_collection(Collection::Principal)
|
|
.update_document(0)
|
|
.value(Property::Parameters, (), F_VALUE | F_CLEAR);
|
|
self.write_batch(batch).await.map_err(|_| {
|
|
Cow::from("Failed to save encryption parameters, please try again later")
|
|
})?;
|
|
Ok(None)
|
|
}
|
|
} 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, "OpenPGP"),
|
|
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"),
|
|
}
|
|
}
|
|
}
|