EE code reorganisation

This commit is contained in:
mdecimus
2024-07-09 09:47:51 +02:00
parent 38ff4b9ea0
commit e683deb74e
19 changed files with 94 additions and 192 deletions

View File

@@ -10,7 +10,6 @@ nlp = { path = "../nlp" }
store = { path = "../store" }
directory = { path = "../directory" }
jmap_proto = { path = "../jmap-proto" }
se_licensing = { path = "../se-licensing", optional = true }
sieve-rs = { version = "0.5" }
mail-parser = { version = "0.9", features = ["full_encoding", "ludicrous_mode"] }
mail-auth = { version = "0.4" }
@@ -67,4 +66,4 @@ tracing-journald = "0.3"
[features]
test_mode = []
enterprise = ["se_licensing"]
enterprise = []

View File

@@ -16,13 +16,6 @@ use crate::{
Network,
};
#[cfg(feature = "enterprise")]
use crate::Enterprise;
#[cfg(feature = "enterprise")]
use jmap_proto::types::collection::Collection;
#[cfg(feature = "enterprise")]
use se_licensing::license::LicenseValidator;
use self::{
imap::ImapConfig, jmap::settings::JmapConfig, scripts::Scripting, smtp::SmtpConfig,
storage::Storage,
@@ -123,60 +116,6 @@ impl Core {
.directories
.insert("*".to_string(), directory.clone());
// SPDX-SnippetBegin
// SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
// SPDX-License-Identifier: LicenseRef-SEL
#[cfg(feature = "enterprise")]
let enterprise = match config.value("enterprise.license-key").map(|key| {
LicenseValidator::new().try_parse(key).and_then(|key| {
key.into_validated_key(config.value("lookup.default.hostname").unwrap_or_default())
})
}) {
Some(Ok(license)) => {
match data
.get_bitmap(store::BitmapKey::document_ids(
u32::MAX,
Collection::Principal,
))
.await
{
Ok(Some(bitmap)) if bitmap.len() > license.accounts as u64 => {
config.new_build_warning(
"enterprise.license-key",
format!(
"License key is valid but only allows {} accounts, found {}.",
license.accounts,
bitmap.len()
),
);
None
}
Err(e) => {
if !matches!(data, Store::None) {
config.new_build_error("enterprise.license-key", e.to_string());
}
None
}
_ => Some(Enterprise {
license,
undelete_period: config
.property_or_default::<Option<std::time::Duration>>(
"enterprise.undelete-period",
"false",
)
.unwrap_or_default(),
}),
}
}
Some(Err(e)) => {
config.new_build_warning("enterprise.license-key", e.to_string());
None
}
None => None,
};
// SPDX-SnippetEnd
// If any of the stores are missing, disable all stores to avoid data loss
if matches!(data, Store::None)
|| matches!(&blob.backend, BlobBackend::Store(Store::None))
@@ -194,6 +133,8 @@ impl Core {
}
Self {
#[cfg(feature = "enterprise")]
enterprise: crate::enterprise::Enterprise::parse(config, &data).await,
sieve: Scripting::parse(config, &stores).await,
network: Network::parse(config),
smtp: SmtpConfig::parse(config).await,
@@ -215,8 +156,6 @@ impl Core {
blobs: stores.blob_stores,
ftss: stores.fts_stores,
},
#[cfg(feature = "enterprise")]
enterprise,
}
}

View File

@@ -0,0 +1,64 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
*
* SPDX-License-Identifier: LicenseRef-SEL
*
* This file is subject to the Stalwart Enterprise License Agreement (SEL) and
* is NOT open source software.
*
*/
use std::time::Duration;
use jmap_proto::types::collection::Collection;
use store::{BitmapKey, Store};
use utils::config::Config;
use super::{license::LicenseValidator, Enterprise};
impl Enterprise {
pub async fn parse(config: &mut Config, data: &Store) -> Option<Self> {
let license = match LicenseValidator::new()
.try_parse(config.value("enterprise.license-key")?)
.and_then(|key| {
key.into_validated_key(config.value("lookup.default.hostname").unwrap_or_default())
}) {
Ok(key) => key,
Err(err) => {
config.new_build_warning("enterprise.license-key", err.to_string());
return None;
}
};
match data
.get_bitmap(BitmapKey::document_ids(u32::MAX, Collection::Principal))
.await
{
Ok(Some(bitmap)) if bitmap.len() > license.accounts as u64 => {
config.new_build_warning(
"enterprise.license-key",
format!(
"License key is valid but only allows {} accounts, found {}.",
license.accounts,
bitmap.len()
),
);
return None;
}
Err(e) => {
if !matches!(data, Store::None) {
config.new_build_error("enterprise.license-key", e.to_string());
}
return None;
}
_ => (),
}
Some(Enterprise {
license,
undelete_period: config
.property_or_default::<Option<Duration>>("enterprise.undelete-period", "false")
.unwrap_or_default(),
})
}
}

View File

@@ -0,0 +1,295 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
*
* SPDX-License-Identifier: LicenseRef-SEL
*
* This file is subject to the Stalwart Enterprise License Agreement (SEL) and
* is NOT open source software.
*
*/
/*
* WARNING: TAMPERING WITH THIS CODE IS STRICTLY PROHIBITED
* Any attempt to modify, bypass, or disable the license validation mechanism
* constitutes a severe violation of the Stalwart Enterprise License Agreement.
* Such actions may result in immediate termination of your license, legal action,
* and substantial financial penalties. Stalwart Labs Ltd. actively monitors for
* unauthorized modifications and will pursue all available legal remedies against
* violators to the fullest extent of the law, including but not limited to claims
* for copyright infringement, breach of contract, and fraud.
*/
use std::{
fmt::{Display, Formatter},
time::{Duration, SystemTime},
};
use ring::signature::{Ed25519KeyPair, UnparsedPublicKey, ED25519};
use base64::{engine::general_purpose::STANDARD, Engine};
pub struct LicenseValidator {
public_key: UnparsedPublicKey<Vec<u8>>,
}
pub struct LicenseGenerator {
key_pair: Ed25519KeyPair,
}
#[derive(Debug, Clone)]
pub struct LicenseKey {
pub valid_to: u64,
pub valid_from: u64,
pub hostname: String,
pub accounts: u32,
}
#[derive(Debug)]
pub enum LicenseError {
Expired,
HostnameMismatch { issued_to: String, current: String },
Parse,
Validation,
Decode,
InvalidParameters,
}
const U64_LEN: usize = std::mem::size_of::<u64>();
const U32_LEN: usize = std::mem::size_of::<u32>();
impl LicenseValidator {
#[allow(clippy::new_without_default)]
pub fn new() -> Self {
LicenseValidator {
public_key: UnparsedPublicKey::new(
&ED25519,
vec![
118, 10, 182, 35, 89, 111, 11, 60, 154, 47, 205, 127, 107, 229, 55, 104, 72,
54, 141, 14, 97, 219, 2, 4, 119, 143, 156, 10, 152, 216, 32, 194,
],
),
}
}
pub fn try_parse(&self, key: impl AsRef<str>) -> Result<LicenseKey, LicenseError> {
let key = STANDARD
.decode(key.as_ref())
.map_err(|_| LicenseError::Decode)?;
let valid_from = u64::from_le_bytes(
key.get(..U64_LEN)
.ok_or(LicenseError::Parse)?
.try_into()
.unwrap(),
);
let valid_to = u64::from_le_bytes(
key.get(U64_LEN..(U64_LEN * 2))
.ok_or(LicenseError::Parse)?
.try_into()
.unwrap(),
);
let accounts = u32::from_le_bytes(
key.get((U64_LEN * 2)..(U64_LEN * 2) + U32_LEN)
.ok_or(LicenseError::Parse)?
.try_into()
.unwrap(),
);
let hostname_len = u32::from_le_bytes(
key.get((U64_LEN * 2) + U32_LEN..(U64_LEN * 2) + (U32_LEN * 2))
.ok_or(LicenseError::Parse)?
.try_into()
.unwrap(),
) as usize;
let hostname = String::from_utf8(
key.get((U64_LEN * 2) + (U32_LEN * 2)..(U64_LEN * 2) + (U32_LEN * 2) + hostname_len)
.ok_or(LicenseError::Parse)?
.to_vec(),
)
.map_err(|_| LicenseError::Parse)?;
let signature = key
.get((U64_LEN * 2) + (U32_LEN * 2) + hostname_len..)
.ok_or(LicenseError::Parse)?;
if valid_from == 0
|| valid_to == 0
|| valid_from >= valid_to
|| accounts == 0
|| hostname.is_empty()
{
return Err(LicenseError::InvalidParameters);
}
// Validate signature
self.public_key
.verify(
&key[..(U64_LEN * 2) + (U32_LEN * 2) + hostname_len],
signature,
)
.map_err(|_| LicenseError::Validation)?;
let key = LicenseKey {
valid_from,
valid_to,
hostname,
accounts,
};
if !key.is_expired() {
Ok(key)
} else {
Err(LicenseError::Expired)
}
}
}
impl LicenseKey {
pub fn new(hostname: String, accounts: u32, expires_in: u64) -> Self {
let now = SystemTime::UNIX_EPOCH
.elapsed()
.unwrap_or_default()
.as_secs();
LicenseKey {
valid_from: now - 300,
valid_to: now + expires_in + 300,
hostname,
accounts,
}
}
pub fn expires_in(&self) -> Duration {
Duration::from_secs(
self.valid_to.saturating_sub(
SystemTime::UNIX_EPOCH
.elapsed()
.unwrap_or_default()
.as_secs(),
),
)
}
pub fn is_expired(&self) -> bool {
let now = SystemTime::UNIX_EPOCH
.elapsed()
.unwrap_or_default()
.as_secs();
now >= self.valid_to || now < self.valid_from
}
pub fn into_validated_key(self, hostname: impl AsRef<str>) -> Result<Self, LicenseError> {
if self.hostname != hostname.as_ref() {
Err(LicenseError::HostnameMismatch {
issued_to: self.hostname.clone(),
current: hostname.as_ref().to_string(),
})
} else {
Ok(self)
}
}
}
impl LicenseGenerator {
pub fn new(pkcs8_der: impl AsRef<[u8]>) -> Self {
Self {
key_pair: Ed25519KeyPair::from_pkcs8(pkcs8_der.as_ref()).unwrap(),
}
}
pub fn generate(&self, key: LicenseKey) -> String {
let mut bytes = Vec::new();
bytes.extend_from_slice(&key.valid_from.to_le_bytes());
bytes.extend_from_slice(&key.valid_to.to_le_bytes());
bytes.extend_from_slice(&key.accounts.to_le_bytes());
bytes.extend_from_slice(&(key.hostname.len() as u32).to_le_bytes());
bytes.extend_from_slice(key.hostname.as_bytes());
bytes.extend_from_slice(self.key_pair.sign(&bytes).as_ref());
STANDARD.encode(&bytes)
}
}
impl Display for LicenseError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
LicenseError::Expired => write!(f, "License is expired"),
LicenseError::Parse => write!(f, "Failed to parse license key"),
LicenseError::Validation => write!(f, "Failed to validate license key"),
LicenseError::Decode => write!(f, "Failed to decode license key"),
LicenseError::InvalidParameters => write!(f, "Invalid license key parameters"),
LicenseError::HostnameMismatch { issued_to, current } => {
write!(
f,
"License issued to {} does not match {}",
issued_to, current
)
}
}
}
}
/*
use rustls::sign::CertifiedKey;
use webpki::TrustAnchor;
use x509_parser::{certificate::X509Certificate, prelude::FromDer};
fn validate_certificate(key: &CertifiedKey) -> Result<(), Box<dyn std::error::Error>> {
let cert_der = key.end_entity_cert()?.as_ref();
webpki::EndEntityCert::try_from(cert_der)?.verify_is_valid_tls_server_cert(
&[
&webpki::ECDSA_P256_SHA256,
&webpki::ECDSA_P256_SHA384,
&webpki::ECDSA_P384_SHA256,
&webpki::ECDSA_P384_SHA384,
&webpki::ED25519,
&webpki::RSA_PKCS1_2048_8192_SHA256,
&webpki::RSA_PKCS1_2048_8192_SHA384,
&webpki::RSA_PKCS1_2048_8192_SHA512,
&webpki::RSA_PKCS1_3072_8192_SHA384,
&webpki::RSA_PSS_2048_8192_SHA256_LEGACY_KEY,
&webpki::RSA_PSS_2048_8192_SHA384_LEGACY_KEY,
&webpki::RSA_PSS_2048_8192_SHA512_LEGACY_KEY,
],
&webpki::TlsServerTrustAnchors(
webpki_roots::TLS_SERVER_ROOTS
.iter()
.map(|ta| TrustAnchor {
subject: ta.subject.as_ref(),
spki: ta.subject_public_key_info.as_ref(),
name_constraints: ta.name_constraints.as_ref().map(|nc| nc.as_ref()),
})
.collect::<Vec<_>>()
.as_slice(),
),
&key.cert
.iter()
.skip(1)
.map(|der| der.as_ref())
.collect::<Vec<_>>(),
webpki::Time::try_from(SystemTime::now())?,
)?;
// Additional checks
let x509 = X509Certificate::from_der(cert_der)?.1;
// Check if self-signed
if x509.issuer() == x509.subject() {
return Err("Certificate is self-signed".into());
}
// Check expiration
let not_before = x509.validity().not_before.timestamp();
let not_after = x509.validity().not_after.timestamp();
let now = SystemTime::UNIX_EPOCH
.elapsed()
.unwrap_or_default()
.as_secs() as i64;
if now < not_before || now > not_after {
Err("Certificate is expired or not yet valid".into())
} else {
Ok(())
}
}
*/

View File

@@ -0,0 +1,58 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
*
* SPDX-License-Identifier: LicenseRef-SEL
*
* This file is subject to the Stalwart Enterprise License Agreement (SEL) and
* is NOT open source software.
*
*/
pub mod config;
pub mod license;
pub mod undelete;
use std::time::Duration;
use license::LicenseKey;
use crate::Core;
#[derive(Clone)]
pub struct Enterprise {
pub license: LicenseKey,
pub undelete_period: Option<Duration>,
}
impl Core {
// WARNING: TAMPERING WITH THIS FUNCTION IS STRICTLY PROHIBITED
// Any attempt to modify, bypass, or disable this license validation mechanism
// constitutes a severe violation of the Stalwart Enterprise License Agreement.
// Such actions may result in immediate termination of your license, legal action,
// and substantial financial penalties. Stalwart Labs Ltd. actively monitors for
// unauthorized modifications and will pursue all available legal remedies against
// violators to the fullest extent of the law, including but not limited to claims
// for copyright infringement, breach of contract, and fraud.
pub fn is_enterprise_edition(&self) -> bool {
self.enterprise
.as_ref()
.map_or(false, |e| !e.license.is_expired())
}
pub fn licensed_accounts(&self) -> u32 {
self.enterprise.as_ref().map_or(0, |e| e.license.accounts)
}
pub fn log_license_details(&self) {
if let Some(enterprise) = &self.enterprise {
tracing::info!(
licensed_to = enterprise.license.hostname,
valid_from = enterprise.license.valid_from,
valid_to = enterprise.license.valid_to,
accounts = enterprise.license.accounts,
"Stalwart Enterprise Edition license key is valid",
);
}
}
}

View File

@@ -0,0 +1,114 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
*
* SPDX-License-Identifier: LicenseRef-SEL
*
* This file is subject to the Stalwart Enterprise License Agreement (SEL) and
* is NOT open source software.
*
*/
use serde::{Deserialize, Serialize};
use store::{
write::{
key::{DeserializeBigEndian, KeySerializer},
now, BatchBuilder, BlobOp, ValueClass,
},
IterateParams, ValueKey, U32_LEN, U64_LEN,
};
use utils::{BlobHash, BLOB_HASH_LEN};
use crate::Core;
#[derive(Debug, Serialize, Deserialize)]
pub struct DeletedBlob<H, T, C> {
pub hash: H,
pub size: usize,
#[serde(rename = "deletedAt")]
pub deleted_at: T,
#[serde(rename = "expiresAt")]
pub expires_at: T,
pub collection: C,
}
impl Core {
pub fn hold_undelete(
&self,
batch: &mut BatchBuilder,
collection: u8,
blob_hash: &BlobHash,
blob_size: usize,
) {
if let Some(hold_period) = self.enterprise.as_ref().and_then(|e| e.undelete_period) {
let now = now();
batch.set(
BlobOp::Reserve {
hash: blob_hash.clone(),
until: now + hold_period.as_secs(),
},
KeySerializer::new(U64_LEN + U64_LEN)
.write(blob_size as u32)
.write(now)
.write(collection)
.finalize(),
);
}
}
pub async fn list_deleted(
&self,
account_id: u32,
) -> store::Result<Vec<DeletedBlob<BlobHash, u64, u8>>> {
let from_key = ValueKey {
account_id,
collection: 0,
document_id: 0,
class: ValueClass::Blob(BlobOp::Reserve {
hash: BlobHash::default(),
until: 0,
}),
};
let to_key = ValueKey {
account_id: account_id + 1,
collection: 0,
document_id: 0,
class: ValueClass::Blob(BlobOp::Reserve {
hash: BlobHash::default(),
until: 0,
}),
};
let now = now();
let mut results = Vec::new();
self.storage
.data
.iterate(
IterateParams::new(from_key, to_key).ascending(),
|key, value| {
let expires_at = key.deserialize_be_u64(key.len() - U64_LEN)?;
if value.len() == U32_LEN + U64_LEN + 1 && expires_at > now {
results.push(DeletedBlob {
hash: BlobHash::try_from_hash_slice(
key.get(U32_LEN..U32_LEN + BLOB_HASH_LEN).ok_or_else(|| {
store::Error::InternalError(format!(
"Invalid key {key:?} in blob hash tables"
))
})?,
)
.unwrap(),
size: value.deserialize_be_u32(0)? as usize,
deleted_at: value.deserialize_be_u64(U32_LEN)?,
expires_at,
collection: *value.last().unwrap(),
});
}
Ok(true)
},
)
.await?;
Ok(results)
}
}

View File

@@ -35,8 +35,6 @@ use opentelemetry_sdk::{
Resource,
};
use opentelemetry_semantic_conventions::resource::{SERVICE_NAME, SERVICE_VERSION};
#[cfg(feature = "enterprise")]
use se_licensing::license::LicenseKey;
use sieve::Sieve;
use store::LookupStore;
use tokio::sync::{mpsc, oneshot};
@@ -49,6 +47,8 @@ use webhooks::{manager::WebhookEvent, WebhookPayload, WebhookType, Webhooks};
pub mod addresses;
pub mod config;
#[cfg(feature = "enterprise")]
pub mod enterprise;
pub mod expr;
pub mod listener;
pub mod manager;
@@ -73,7 +73,7 @@ pub struct Core {
pub imap: ImapConfig,
pub web_hooks: Webhooks,
#[cfg(feature = "enterprise")]
pub enterprise: Option<Enterprise>,
pub enterprise: Option<enterprise::Enterprise>,
}
#[derive(Clone)]
@@ -83,19 +83,6 @@ pub struct Network {
pub url: IfBlock,
}
// SPDX-SnippetBegin
// SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
// SPDX-License-Identifier: LicenseRef-SEL
#[cfg(feature = "enterprise")]
#[derive(Clone)]
pub struct Enterprise {
pub license: LicenseKey,
pub undelete_period: Option<std::time::Duration>,
}
// SPDX-SnippetEnd
pub enum AuthResult<T> {
Success(T),
Failure(AuthFailureReason),