diff --git a/CHANGELOG.md b/CHANGELOG.md index cdc3fc6b..1bf07a42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ All notable changes to this project will be documented in this file. This projec If you are upgrading from v0.16.x, replace the binary (or run `docker pull`). If you are upgrading from v0.15.x and below, please read the [upgrading documentation](https://github.com/stalwartlabs/stalwart/blob/main/UPGRADING/v0_16.md) for more information on how to upgrade from previous versions. ## Added +- Encryption-at-rest: Support for `AES-256-GCM` and `ChaCha20-Poly1305` for S/MIME (#161). - S3: Support for `allowInvalidCerts` option to allow connecting to S3 endpoints with invalid TLS certificates. ## Changed diff --git a/Cargo.lock b/Cargo.lock index 6849149f..8aec4406 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1116,6 +1116,17 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher 0.4.4", + "cpufeatures 0.2.17", +] + [[package]] name = "chacha20" version = "0.10.1" @@ -1127,6 +1138,19 @@ dependencies = [ "rand_core 0.10.1", ] +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20 0.9.1", + "cipher 0.4.4", + "poly1305", + "zeroize", +] + [[package]] name = "chrono" version = "0.4.45" @@ -2263,6 +2287,7 @@ dependencies = [ "aes-gcm", "aes-gcm-siv", "cbc 0.2.1", + "chacha20poly1305", "common", "compact_str", "directory", @@ -5528,6 +5553,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures 0.2.17", + "opaque-debug", + "universal-hash", +] + [[package]] name = "polyval" version = "0.6.2" @@ -6037,7 +6073,7 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" dependencies = [ - "chacha20", + "chacha20 0.10.1", "getrandom 0.4.3", "rand_core 0.10.1", ] @@ -6591,6 +6627,7 @@ dependencies = [ "pkcs1", "pkcs8", "rand_core 0.6.4", + "sha2 0.10.9", "signature", "spki", "subtle", diff --git a/crates/common/src/auth/mod.rs b/crates/common/src/auth/mod.rs index 9cdb656f..bfd4b373 100644 --- a/crates/common/src/auth/mod.rs +++ b/crates/common/src/auth/mod.rs @@ -94,6 +94,8 @@ pub const ACCOUNT_FLAG_ENCRYPT_METHOD_PGP: u64 = 1 << 3; pub const ACCOUNT_FLAG_ENCRYPT_ALGO_AES256: u64 = 1 << 4; pub const ACCOUNT_FLAG_ENCRYPT_ALGO_AES128: u64 = 1 << 5; pub const ACCOUNT_FLAG_ENCRYPT_APPEND: u64 = 1 << 6; +pub const ACCOUNT_FLAG_ENCRYPT_ALGO_AES256_GCM: u64 = 1 << 7; +pub const ACCOUNT_FLAG_ENCRYPT_ALGO_CHACHA20_POLY1305: u64 = 1 << 8; #[derive(Debug, Clone)] pub struct RoleCache { diff --git a/crates/common/src/cache/principals.rs b/crates/common/src/cache/principals.rs index 17f8ee81..234a2ed5 100644 --- a/crates/common/src/cache/principals.rs +++ b/crates/common/src/cache/principals.rs @@ -8,6 +8,7 @@ use crate::{ Server, auth::{ ACCOUNT_FLAG_ENCRYPT_ALGO_AES128, ACCOUNT_FLAG_ENCRYPT_ALGO_AES256, + ACCOUNT_FLAG_ENCRYPT_ALGO_AES256_GCM, ACCOUNT_FLAG_ENCRYPT_ALGO_CHACHA20_POLY1305, ACCOUNT_FLAG_ENCRYPT_APPEND, ACCOUNT_FLAG_ENCRYPT_METHOD_PGP, ACCOUNT_FLAG_ENCRYPT_METHOD_SMIME, ACCOUNT_FLAG_ENCRYPT_TRAIN_SPAM_FILTER, ACCOUNT_IS_USER, AccountCache, AccountInfo, AccountTenantIds, DOMAIN_FLAG_RELAY, DOMAIN_FLAG_SUB_ADDRESSING, @@ -412,6 +413,14 @@ impl Server { flags |= ACCOUNT_FLAG_ENCRYPT_ALGO_AES128; settings.into() } + EncryptionAtRest::Aes256Gcm(settings) => { + flags |= ACCOUNT_FLAG_ENCRYPT_ALGO_AES256_GCM; + settings.into() + } + EncryptionAtRest::ChaCha20Poly1305(settings) => { + flags |= ACCOUNT_FLAG_ENCRYPT_ALGO_CHACHA20_POLY1305; + settings.into() + } }; let encryption_key = if let Some(settings) = encryption_settings { if settings.allow_spam_training { diff --git a/crates/email/Cargo.toml b/crates/email/Cargo.toml index 47d2f4ad..956c3b2d 100644 --- a/crates/email/Cargo.toml +++ b/crates/email/Cargo.toml @@ -24,11 +24,12 @@ serde_json = "1.0" aes = "0.9" aes-gcm = "0.10.1" aes-gcm-siv = "0.11.1" +chacha20poly1305 = "0.10" cbc = { version = "0.2", features = ["alloc"] } rasn = "0.28" rasn-cms = "0.28" rasn-pkix = "0.28" -rsa = "0.9.2" +rsa = { version = "0.9.2", features = ["sha2"] } rand = "0.8" sequoia-openpgp = { version = "2.0", default-features = false, features = ["crypto-rust", "allow-experimental-crypto", "allow-variable-time-crypto"] } hashify = "0.2" diff --git a/crates/email/src/message/crypto.rs b/crates/email/src/message/crypto.rs index 2b90520e..1c050e1f 100644 --- a/crates/email/src/message/crypto.rs +++ b/crates/email/src/message/crypto.rs @@ -5,8 +5,14 @@ */ use aes::cipher::{BlockModeEncrypt, KeyIvInit, block_padding::Pkcs7}; +use aes_gcm::{ + Aes256Gcm, + aead::{AeadInPlace, KeyInit, generic_array::GenericArray}, +}; +use chacha20poly1305::ChaCha20Poly1305; use common::auth::{ - ACCOUNT_FLAG_ENCRYPT_ALGO_AES256, ACCOUNT_FLAG_ENCRYPT_METHOD_PGP, + ACCOUNT_FLAG_ENCRYPT_ALGO_AES256, ACCOUNT_FLAG_ENCRYPT_ALGO_AES256_GCM, + ACCOUNT_FLAG_ENCRYPT_ALGO_CHACHA20_POLY1305, ACCOUNT_FLAG_ENCRYPT_METHOD_PGP, ACCOUNT_FLAG_ENCRYPT_TRAIN_SPAM_FILTER, EncryptionKeys, }; use mail_builder::{encoders::base64::base64_encode_mime, mime::make_boundary}; @@ -17,18 +23,29 @@ use openpgp::{ types::{KeyFlags, SymmetricAlgorithm}, }; use rand::{RngCore, SeedableRng, rngs::StdRng}; -use rasn::types::{ObjectIdentifier, OctetString, SetOf}; +use rasn::Encoder; +use rasn::types::{OctetString, Oid, SetOf}; use rasn_cms::{ - AlgorithmIdentifier, CONTENT_DATA, CONTENT_ENVELOPED_DATA, EncryptedContent, + AlgorithmIdentifier, AuthEnvelopedData, CONTENT_DATA, CONTENT_ENVELOPED_DATA, EncryptedContent, EncryptedContentInfo, EncryptedKey, EnvelopedData, IssuerAndSerialNumber, KeyTransRecipientInfo, RecipientIdentifier, RecipientInfo, algorithms::{AES128_CBC, AES256_CBC, RSA}, pkcs7_compat::EncapsulatedContentInfo, }; -use rsa::{Pkcs1v15Encrypt, RsaPublicKey, pkcs1::DecodeRsaPublicKey}; +use rsa::{Oaep, Pkcs1v15Encrypt, RsaPublicKey, pkcs1::DecodeRsaPublicKey, sha2::Sha256}; use sequoia_openpgp as openpgp; use std::io::Cursor; +const AES256_GCM: &Oid = + Oid::JOINT_ISO_ITU_T_COUNTRY_US_ORGANIZATION_GOV_CSOR_NIST_ALGORITHMS_AES256_GCM; +const CHACHA20_POLY1305: &Oid = Oid::const_new(&[1, 2, 840, 113549, 1, 9, 16, 3, 18]); +const CONTENT_AUTH_ENVELOPED_DATA: &Oid = + Oid::ISO_MEMBER_BODY_US_RSADSI_PKCS9_SMIME_CT_AUTH_ENVELOPED_DATA; +const SHA256: &Oid = + Oid::JOINT_ISO_ITU_T_COUNTRY_US_ORGANIZATION_GOV_CSOR_NIST_ALGORITHMS_HASH_SHA256; +const MGF1: &Oid = Oid::ISO_MEMBER_BODY_US_RSADSI_PKCS1_MGF1; +const RSAES_OAEP: &Oid = Oid::ISO_MEMBER_BODY_US_RSADSI_PKCS1_RSAES_OAEP; + #[derive(Debug)] pub enum EncryptMessageError { AlreadyEncrypted, @@ -51,6 +68,13 @@ impl EncryptMessage for Message<'_> { keys: &EncryptionKeys, flags: u64, ) -> Result, EncryptMessageError> { + if flags & ACCOUNT_FLAG_ENCRYPT_METHOD_PGP != 0 && flags.cipher().is_aead() { + return Err(EncryptMessageError::Error( + "AES-256-GCM and ChaCha20-Poly1305 are only supported for S/MIME encryption." + .into(), + )); + } + 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); @@ -180,18 +204,21 @@ impl EncryptMessage for Message<'_> { outer_message.extend_from_slice(boundary.as_bytes()); outer_message.extend_from_slice(b"--\r\n"); } else { - // Generate random IV + let cipher = flags.cipher(); + + // Generate random nonce let mut rng = StdRng::from_entropy(); - let mut iv = vec![0u8; 16]; - rng.fill_bytes(&mut iv); + let mut nonce = vec![0u8; cipher.nonce_size()]; + rng.fill_bytes(&mut nonce); // Generate random key - let mut key = vec![0u8; flags.key_size()]; + let mut key = vec![0u8; cipher.key_size()]; rng.fill_bytes(&mut key); // Encrypt contents (TODO: use rayon) - let (encrypted_contents, key, iv) = tokio::task::spawn_blocking(move || { - (flags.encrypt(&key, &iv, &inner_message), key, iv) + let (encrypted_contents, mac, key, nonce) = tokio::task::spawn_blocking(move || { + let (encrypted_contents, mac) = cipher.encrypt(&key, &nonce, &inner_message); + (encrypted_contents, mac, key, nonce) }) .await .map_err(|err| { @@ -199,6 +226,7 @@ impl EncryptMessage for Message<'_> { })?; // Encrypt key using public keys + let key_encryption_algorithm = cipher.key_encryption_algorithm()?; let mut recipient_infos = SetOf::new(); for cert in keys.iter() { let cert = rasn::der::decode::(cert).map_err(|err| { @@ -214,12 +242,14 @@ impl EncryptMessage for Message<'_> { .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(); + let encrypted_key = if cipher.is_aead() { + public_key.encrypt(&mut rng, Oaep::new::(), &key[..]) + } else { + public_key.encrypt(&mut rng, Pkcs1v15Encrypt, &key[..]) + } + .map_err(|err| { + EncryptMessageError::Error(format!("Failed to encrypt key: {}", err)) + })?; recipient_infos.insert(RecipientInfo::KeyTransRecipientInfo( KeyTransRecipientInfo { @@ -228,48 +258,45 @@ impl EncryptMessage for Message<'_> { 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(), - ), - }, + key_encryption_algorithm: key_encryption_algorithm.clone(), encrypted_key: EncryptedKey::from(encrypted_key), }, )); } - let pkcs7 = rasn::der::encode(&EncapsulatedContentInfo { - content_type: CONTENT_ENVELOPED_DATA.into(), - content: Some( + let encrypted_content_info = EncryptedContentInfo { + content_type: CONTENT_DATA.into(), + content_encryption_algorithm: cipher.content_encryption_algorithm(&nonce)?, + encrypted_content: Some(EncryptedContent::from(encrypted_contents)), + }; + + let (content_type, content) = if let Some(mac) = mac { + ( + CONTENT_AUTH_ENVELOPED_DATA, + rasn::der::encode(&AuthEnvelopedData { + version: 0.into(), + originator_info: None, + recipient_infos, + auth_encrypted_content_info: encrypted_content_info, + auth_attrs: None, + mac: OctetString::from(mac), + unauth_attrs: None, + }) + .map_err(|err| { + EncryptMessageError::Error(format!( + "Failed to encode AuthEnvelopedData: {}", + err + )) + })?, + ) + } else { + ( + CONTENT_ENVELOPED_DATA, 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: flags.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)), - }, + encrypted_content_info, unprotected_attrs: None, }) .map_err(|err| { @@ -277,20 +304,28 @@ impl EncryptMessage for Message<'_> { "Failed to encode EnvelopedData: {}", err )) - })? - .into(), - ), + })?, + ) + }; + + let pkcs7 = rasn::der::encode(&EncapsulatedContentInfo { + content_type: content_type.into(), + content: Some(content.into()), }) .map_err(|err| { EncryptMessageError::Error(format!("Failed to encode ContentInfo: {}", err)) })?; // Generate message + outer_message.extend_from_slice(b"Content-Type: application/pkcs7-mime;\r\n"); + outer_message.extend_from_slice(b"\tname=\"smime.p7m\";\r\n\tsmime-type="); + outer_message.extend_from_slice(if cipher.is_aead() { + b"authenticated-enveloped-data\r\n" + } else { + b"enveloped-data\r\n" + }); 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" @@ -360,27 +395,21 @@ impl EncryptMessage for Message<'_> { } pub trait EncryptionFlags { - fn key_size(&self) -> usize; - fn to_algorithm_identifier(&self) -> ObjectIdentifier; + fn cipher(&self) -> SymmetricCipher; fn can_train_spam_filter(&self) -> bool; - fn encrypt(&self, key: &[u8], iv: &[u8], contents: &[u8]) -> Vec; fn algo(&self) -> SymmetricAlgorithm; } impl EncryptionFlags for u64 { - fn key_size(&self) -> usize { - if *self & ACCOUNT_FLAG_ENCRYPT_ALGO_AES256 != 0 { - 32 + fn cipher(&self) -> SymmetricCipher { + if *self & ACCOUNT_FLAG_ENCRYPT_ALGO_AES256_GCM != 0 { + SymmetricCipher::Aes256Gcm + } else if *self & ACCOUNT_FLAG_ENCRYPT_ALGO_CHACHA20_POLY1305 != 0 { + SymmetricCipher::ChaCha20Poly1305 + } else if *self & ACCOUNT_FLAG_ENCRYPT_ALGO_AES256 != 0 { + SymmetricCipher::Aes256Cbc } else { - 16 - } - } - - fn to_algorithm_identifier(&self) -> ObjectIdentifier { - if *self & ACCOUNT_FLAG_ENCRYPT_ALGO_AES256 != 0 { - AES256_CBC.into() - } else { - AES128_CBC.into() + SymmetricCipher::Aes128Cbc } } @@ -388,18 +417,6 @@ impl EncryptionFlags for u64 { *self & ACCOUNT_FLAG_ENCRYPT_TRAIN_SPAM_FILTER != 0 } - fn encrypt(&self, key: &[u8], iv: &[u8], contents: &[u8]) -> Vec { - if *self & ACCOUNT_FLAG_ENCRYPT_ALGO_AES256 != 0 { - cbc::Encryptor::::new_from_slices(key, iv) - .expect("invalid key or iv length") - .encrypt_padded_vec::(contents) - } else { - cbc::Encryptor::::new_from_slices(key, iv) - .expect("invalid key or iv length") - .encrypt_padded_vec::(contents) - } - } - fn algo(&self) -> SymmetricAlgorithm { if *self & ACCOUNT_FLAG_ENCRYPT_ALGO_AES256 != 0 { SymmetricAlgorithm::AES256 @@ -408,3 +425,158 @@ impl EncryptionFlags for u64 { } } } + +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum SymmetricCipher { + Aes128Cbc, + Aes256Cbc, + Aes256Gcm, + ChaCha20Poly1305, +} + +impl SymmetricCipher { + fn key_size(self) -> usize { + match self { + SymmetricCipher::Aes128Cbc => 16, + SymmetricCipher::Aes256Cbc + | SymmetricCipher::Aes256Gcm + | SymmetricCipher::ChaCha20Poly1305 => 32, + } + } + + fn nonce_size(self) -> usize { + match self { + SymmetricCipher::Aes128Cbc | SymmetricCipher::Aes256Cbc => 16, + SymmetricCipher::Aes256Gcm | SymmetricCipher::ChaCha20Poly1305 => 12, + } + } + + fn is_aead(self) -> bool { + matches!( + self, + SymmetricCipher::Aes256Gcm | SymmetricCipher::ChaCha20Poly1305 + ) + } + + fn encrypt(self, key: &[u8], nonce: &[u8], contents: &[u8]) -> (Vec, Option>) { + match self { + SymmetricCipher::Aes128Cbc => ( + cbc::Encryptor::::new_from_slices(key, nonce) + .expect("invalid key or iv length") + .encrypt_padded_vec::(contents), + None, + ), + SymmetricCipher::Aes256Cbc => ( + cbc::Encryptor::::new_from_slices(key, nonce) + .expect("invalid key or iv length") + .encrypt_padded_vec::(contents), + None, + ), + SymmetricCipher::Aes256Gcm => { + let cipher = Aes256Gcm::new_from_slice(key).expect("invalid key length"); + let mut buffer = contents.to_vec(); + let tag = cipher + .encrypt_in_place_detached(GenericArray::from_slice(nonce), b"", &mut buffer) + .expect("AES-GCM encryption failed"); + (buffer, Some(tag.to_vec())) + } + SymmetricCipher::ChaCha20Poly1305 => { + let cipher = ChaCha20Poly1305::new_from_slice(key).expect("invalid key length"); + let mut buffer = contents.to_vec(); + let tag = cipher + .encrypt_in_place_detached(GenericArray::from_slice(nonce), b"", &mut buffer) + .expect("ChaCha20-Poly1305 encryption failed"); + (buffer, Some(tag.to_vec())) + } + } + } + + fn content_encryption_algorithm( + self, + nonce: &[u8], + ) -> Result { + let (algorithm, parameters) = match self { + SymmetricCipher::Aes128Cbc => (AES128_CBC, encode_octet_string(nonce)?), + SymmetricCipher::Aes256Cbc => (AES256_CBC, encode_octet_string(nonce)?), + SymmetricCipher::ChaCha20Poly1305 => (CHACHA20_POLY1305, encode_octet_string(nonce)?), + SymmetricCipher::Aes256Gcm => ( + AES256_GCM, + rasn::der::encode(&GcmParameters { + nonce: OctetString::from_slice(nonce), + icv_len: 16, + }) + .map_err(|err| { + EncryptMessageError::Error(format!("Failed to encode GCM parameters: {}", err)) + })?, + ), + }; + + Ok(AlgorithmIdentifier { + algorithm: algorithm.into(), + parameters: Some(parameters.into()), + }) + } + + fn key_encryption_algorithm(self) -> Result { + if self.is_aead() { + let sha256 = AlgorithmIdentifier { + algorithm: SHA256.into(), + parameters: Some(encode_null()?.into()), + }; + let parameters = rasn::der::encode(&OaepParameters { + hash_algorithm: sha256.clone(), + mask_gen_algorithm: AlgorithmIdentifier { + algorithm: MGF1.into(), + parameters: Some( + rasn::der::encode(&sha256) + .map_err(|err| { + EncryptMessageError::Error(format!( + "Failed to encode MGF1 parameters: {}", + err + )) + })? + .into(), + ), + }, + }) + .map_err(|err| { + EncryptMessageError::Error(format!("Failed to encode OAEP parameters: {}", err)) + })?; + + Ok(AlgorithmIdentifier { + algorithm: RSAES_OAEP.into(), + parameters: Some(parameters.into()), + }) + } else { + Ok(AlgorithmIdentifier { + algorithm: RSA.into(), + parameters: Some(encode_null()?.into()), + }) + } + } +} + +#[derive(rasn::AsnType, rasn::Encode)] +struct GcmParameters { + nonce: OctetString, + icv_len: u8, +} + +#[derive(rasn::AsnType, rasn::Encode)] +struct OaepParameters { + #[rasn(tag(explicit(0)))] + hash_algorithm: AlgorithmIdentifier, + #[rasn(tag(explicit(1)))] + mask_gen_algorithm: AlgorithmIdentifier, +} + +fn encode_octet_string(value: &[u8]) -> Result, EncryptMessageError> { + rasn::der::encode(&OctetString::from_slice(value)) + .map_err(|err| EncryptMessageError::Error(format!("Failed to encode nonce: {}", err))) +} + +fn encode_null() -> Result, EncryptMessageError> { + rasn::der::encode(&()).map_err(|err| { + EncryptMessageError::Error(format!("Failed to encode NULL parameters: {}", err)) + }) +} diff --git a/crates/jmap/src/registry/mapping/account.rs b/crates/jmap/src/registry/mapping/account.rs index ecd4139d..ab8aa52d 100644 --- a/crates/jmap/src/registry/mapping/account.rs +++ b/crates/jmap/src/registry/mapping/account.rs @@ -16,6 +16,7 @@ use crate::{ }, }; use common::{ + Server, auth::{ AccessToken, Permissions, PermissionsGroup, credential::{ApiKey, AppPassword}, @@ -23,6 +24,7 @@ use common::{ }, cache::invalidate::CacheInvalidationBuilder, ipc::CacheInvalidation, + storage::encryption::{EncryptionMethod, parse_public_key}, }; use directory::core::secret::{SecretVerificationResult, hash_secret, verify_mfa_secret_hash}; use jmap_proto::{error::set::SetError, request::MaybeInvalid, types::state::State}; @@ -33,8 +35,8 @@ use registry::{ enums::{CredentialType, StorageQuota}, prelude::{MASKED_PASSWORD, Object, ObjectInner, ObjectType, Property}, structs::{ - Account, AccountPassword, AccountSettings, Credential, CredentialPermissions, OtpAuth, - SecondaryCredential, + Account, AccountPassword, AccountSettings, Credential, CredentialPermissions, + EncryptionAtRest, OtpAuth, PublicKey, SecondaryCredential, }, }, types::{datetime::UTCDateTime, id::ObjectId}, @@ -105,6 +107,22 @@ pub(crate) async fn account_set( } } + if account.encryption_at_rest != old_account.encryption_at_rest + && let Some(algorithm) = + unsupported_pgp_algorithm(set.server, &account.encryption_at_rest).await? + { + account = old_account.clone(); + set.response.not_updated.append( + id, + SetError::invalid_properties() + .with_property(Property::EncryptionAtRest) + .with_description(format!( + "{algorithm} is only supported for S/MIME encryption, but the selected public key is an OpenPGP key." + )), + ); + break 'outer; + } + set.response.updated.append(id, None); } } @@ -897,6 +915,32 @@ pub(crate) async fn credential_query( Ok(response) } +async fn unsupported_pgp_algorithm( + server: &Server, + encryption_at_rest: &EncryptionAtRest, +) -> trc::Result> { + let (settings, algorithm) = match encryption_at_rest { + EncryptionAtRest::Aes256Gcm(settings) => (settings, "AES-256-GCM"), + EncryptionAtRest::ChaCha20Poly1305(settings) => (settings, "ChaCha20-Poly1305"), + _ => return Ok(None), + }; + + if let Some(public_key) = server + .registry() + .object::(settings.public_key) + .await + .caused_by(trc::location!())? + && matches!( + parse_public_key(&public_key), + Ok(Some(params)) if params.method == EncryptionMethod::PGP + ) + { + Ok(Some(algorithm)) + } else { + Ok(None) + } +} + pub(crate) fn validate_credential_permissions( access_token: &AccessToken, credential: &SecondaryCredential, diff --git a/crates/registry/src/schema/enums.rs b/crates/registry/src/schema/enums.rs index a311b330..68822b80 100644 --- a/crates/registry/src/schema/enums.rs +++ b/crates/registry/src/schema/enums.rs @@ -681,6 +681,8 @@ pub enum EncryptionAtRestType { Disabled = 0, Aes128 = 1, Aes256 = 2, + Aes256Gcm = 3, + ChaCha20Poly1305 = 4, } #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)] diff --git a/crates/registry/src/schema/enums_impl.rs b/crates/registry/src/schema/enums_impl.rs index 72ac8ca4..b33bb5a0 100644 --- a/crates/registry/src/schema/enums_impl.rs +++ b/crates/registry/src/schema/enums_impl.rs @@ -3349,6 +3349,8 @@ impl EnumImpl for EncryptionAtRestType { b"Disabled" => EncryptionAtRestType::Disabled, b"Aes128" => EncryptionAtRestType::Aes128, b"Aes256" => EncryptionAtRestType::Aes256, + b"Aes256Gcm" => EncryptionAtRestType::Aes256Gcm, + b"ChaCha20Poly1305" => EncryptionAtRestType::ChaCha20Poly1305, } } @@ -3357,6 +3359,8 @@ impl EnumImpl for EncryptionAtRestType { EncryptionAtRestType::Disabled => "Disabled", EncryptionAtRestType::Aes128 => "Aes128", EncryptionAtRestType::Aes256 => "Aes256", + EncryptionAtRestType::Aes256Gcm => "Aes256Gcm", + EncryptionAtRestType::ChaCha20Poly1305 => "ChaCha20Poly1305", } } @@ -3369,11 +3373,13 @@ impl EnumImpl for EncryptionAtRestType { 0 => Some(EncryptionAtRestType::Disabled), 1 => Some(EncryptionAtRestType::Aes128), 2 => Some(EncryptionAtRestType::Aes256), + 3 => Some(EncryptionAtRestType::Aes256Gcm), + 4 => Some(EncryptionAtRestType::ChaCha20Poly1305), _ => None, } } - const COUNT: usize = 3; + const COUNT: usize = 5; } impl serde::Serialize for EncryptionAtRestType { diff --git a/crates/registry/src/schema/structs.rs b/crates/registry/src/schema/structs.rs index ac046613..b0ef5be9 100644 --- a/crates/registry/src/schema/structs.rs +++ b/crates/registry/src/schema/structs.rs @@ -2785,6 +2785,8 @@ pub enum EncryptionAtRest { Disabled, Aes128(EncryptionSettings), Aes256(EncryptionSettings), + Aes256Gcm(EncryptionSettings), + ChaCha20Poly1305(EncryptionSettings), } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] diff --git a/crates/registry/src/schema/structs_impl.rs b/crates/registry/src/schema/structs_impl.rs index 2e09f31a..e0c718d0 100644 --- a/crates/registry/src/schema/structs_impl.rs +++ b/crates/registry/src/schema/structs_impl.rs @@ -20233,6 +20233,8 @@ impl EncryptionAtRest { EncryptionAtRest::Disabled => true, EncryptionAtRest::Aes128(inner) => inner.validate(errors), EncryptionAtRest::Aes256(inner) => inner.validate(errors), + EncryptionAtRest::Aes256Gcm(inner) => inner.validate(errors), + EncryptionAtRest::ChaCha20Poly1305(inner) => inner.validate(errors), } } @@ -20245,6 +20247,12 @@ impl EncryptionAtRest { EncryptionAtRest::Aes256(object) => { object.index(i); } + EncryptionAtRest::Aes256Gcm(object) => { + object.index(i); + } + EncryptionAtRest::ChaCha20Poly1305(object) => { + object.index(i); + } } } } @@ -20269,6 +20277,14 @@ impl Pickle for EncryptionAtRest { 2u16.pickle(out); inner.pickle(out); } + EncryptionAtRest::Aes256Gcm(inner) => { + 3u16.pickle(out); + inner.pickle(out); + } + EncryptionAtRest::ChaCha20Poly1305(inner) => { + 4u16.pickle(out); + inner.pickle(out); + } } } @@ -20277,6 +20293,8 @@ impl Pickle for EncryptionAtRest { 0 => Some(EncryptionAtRest::Disabled), 1 => Pickle::unpickle(stream).map(EncryptionAtRest::Aes128), 2 => Pickle::unpickle(stream).map(EncryptionAtRest::Aes256), + 3 => Pickle::unpickle(stream).map(EncryptionAtRest::Aes256Gcm), + 4 => Pickle::unpickle(stream).map(EncryptionAtRest::ChaCha20Poly1305), _ => None, } } @@ -20304,6 +20322,20 @@ impl IntoValue for EncryptionAtRest { .insert_unchecked(Property::Type, JmapValue::Str("Aes256".into())); obj } + EncryptionAtRest::Aes256Gcm(obj) => { + let mut obj = obj.into_value(); + obj.as_object_mut() + .unwrap() + .insert_unchecked(Property::Type, JmapValue::Str("Aes256Gcm".into())); + obj + } + EncryptionAtRest::ChaCha20Poly1305(obj) => { + let mut obj = obj.into_value(); + obj.as_object_mut() + .unwrap() + .insert_unchecked(Property::Type, JmapValue::Str("ChaCha20Poly1305".into())); + obj + } } } } @@ -20323,12 +20355,20 @@ impl RegistryJsonPatch for EncryptionAtRest { EncryptionAtRestType::Aes256 => { *self = EncryptionAtRest::Aes256(Default::default()) } + EncryptionAtRestType::Aes256Gcm => { + *self = EncryptionAtRest::Aes256Gcm(Default::default()) + } + EncryptionAtRestType::ChaCha20Poly1305 => { + *self = EncryptionAtRest::ChaCha20Poly1305(Default::default()) + } } } match self { EncryptionAtRest::Disabled => pointer.assert_eof(), EncryptionAtRest::Aes128(inner) => inner.patch(pointer, value), EncryptionAtRest::Aes256(inner) => inner.patch(pointer, value), + EncryptionAtRest::Aes256Gcm(inner) => inner.patch(pointer, value), + EncryptionAtRest::ChaCha20Poly1305(inner) => inner.patch(pointer, value), } } } @@ -20339,6 +20379,8 @@ impl EncryptionAtRest { EncryptionAtRest::Disabled => EncryptionAtRestType::Disabled, EncryptionAtRest::Aes128(_) => EncryptionAtRestType::Aes128, EncryptionAtRest::Aes256(_) => EncryptionAtRestType::Aes256, + EncryptionAtRest::Aes256Gcm(_) => EncryptionAtRestType::Aes256Gcm, + EncryptionAtRest::ChaCha20Poly1305(_) => EncryptionAtRestType::ChaCha20Poly1305, } } } diff --git a/resources/schema/schema.json.gz b/resources/schema/schema.json.gz index ad6410ed..d2bcb825 100644 Binary files a/resources/schema/schema.json.gz and b/resources/schema/schema.json.gz differ diff --git a/resources/schema/schema.json.sha256 b/resources/schema/schema.json.sha256 index 155e3eb5..efdf76f6 100644 --- a/resources/schema/schema.json.sha256 +++ b/resources/schema/schema.json.sha256 @@ -1 +1 @@ -sg9itmRsh5CTBp8BzWUHm41BsC2xIgtZEHM1L7Gy4fQ \ No newline at end of file +h8p5wN6778ClyWk8ZeLEzHFmF2ujSAjRDQbktY0FJj8 \ No newline at end of file