1
0
Fork 0
mirror of https://github.com/dani-garcia/vaultwarden.git synced 2025-06-10 13:03:55 +00:00

Add support for sending email via AWS SES

This commit is contained in:
Chase Douglas 2025-05-29 16:19:26 -07:00
parent 90f7e5ff80
commit 5b294b7f88
5 changed files with 340 additions and 32 deletions

277
Cargo.lock generated
View file

@ -418,6 +418,28 @@ dependencies = [
"uuid",
]
[[package]]
name = "aws-sdk-sesv2"
version = "1.81.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31bd9848a6199a2600ddc6cf2298c93b428c903887c6fcfbd89a48e40822ca1"
dependencies = [
"aws-credential-types",
"aws-runtime",
"aws-smithy-async",
"aws-smithy-http",
"aws-smithy-json",
"aws-smithy-runtime",
"aws-smithy-runtime-api",
"aws-smithy-types",
"aws-types",
"bytes",
"fastrand",
"http 0.2.12",
"regex-lite",
"tracing",
]
[[package]]
name = "aws-sdk-sso"
version = "1.68.0"
@ -496,15 +518,20 @@ dependencies = [
"aws-smithy-runtime-api",
"aws-smithy-types",
"bytes",
"crypto-bigint 0.5.5",
"form_urlencoded",
"hex",
"hmac",
"http 0.2.12",
"http 1.3.1",
"p256",
"percent-encoding",
"ring",
"sha2",
"subtle",
"time",
"tracing",
"zeroize",
]
[[package]]
@ -547,14 +574,19 @@ dependencies = [
"aws-smithy-async",
"aws-smithy-runtime-api",
"aws-smithy-types",
"h2",
"h2 0.4.10",
"http 0.2.12",
"http 1.3.1",
"http-body 0.4.6",
"hyper 0.14.32",
"hyper 1.6.0",
"hyper-rustls",
"hyper-rustls 0.24.2",
"hyper-rustls 0.27.6",
"hyper-util",
"pin-project-lite",
"rustls 0.21.12",
"rustls 0.23.27",
"rustls-native-certs",
"rustls-native-certs 0.8.1",
"rustls-pki-types",
"tokio",
"tower",
@ -639,6 +671,7 @@ dependencies = [
"base64-simd",
"bytes",
"bytes-utils",
"futures-core",
"http 0.2.12",
"http 1.3.1",
"http-body 0.4.6",
@ -651,6 +684,8 @@ dependencies = [
"ryu",
"serde",
"time",
"tokio",
"tokio-util",
]
[[package]]
@ -702,6 +737,12 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "base16ct"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "349a06037c7bf932dd7e7d1f653678b2038b9ad46a74102f1fc7bd7872678cce"
[[package]]
name = "base64"
version = "0.13.1"
@ -1202,6 +1243,28 @@ version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929"
[[package]]
name = "crypto-bigint"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef2b4b23cddf68b89b8f8069890e8c270d54e2d5fe1b143820234805e4cb17ef"
dependencies = [
"generic-array",
"rand_core 0.6.4",
"subtle",
"zeroize",
]
[[package]]
name = "crypto-bigint"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76"
dependencies = [
"rand_core 0.6.4",
"subtle",
]
[[package]]
name = "crypto-common"
version = "0.1.6"
@ -1273,6 +1336,16 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c297a1c74b71ae29df00c3e22dd9534821d60eb9af5a0192823fa2acea70c2a"
[[package]]
name = "der"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1a467a65c5e759bce6e65eaf91cc29f466cdc57cb65777bd646872a8a1fd4de"
dependencies = [
"const-oid",
"zeroize",
]
[[package]]
name = "der"
version = "0.7.9"
@ -1523,12 +1596,44 @@ version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
[[package]]
name = "ecdsa"
version = "0.14.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "413301934810f597c1d19ca71c8710e99a3f1ba28a0d2ebc01551a2daeea3c5c"
dependencies = [
"der 0.6.1",
"elliptic-curve",
"rfc6979",
"signature 1.6.4",
]
[[package]]
name = "either"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]]
name = "elliptic-curve"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7bb888ab5300a19b8e5bceef25ac745ad065f3c9f7efc6de1b91958110891d3"
dependencies = [
"base16ct",
"crypto-bigint 0.4.9",
"der 0.6.1",
"digest",
"ff",
"generic-array",
"group",
"pkcs8 0.9.0",
"rand_core 0.6.4",
"sec1",
"subtle",
"zeroize",
]
[[package]]
name = "email-encoding"
version = "0.4.1"
@ -1636,6 +1741,16 @@ dependencies = [
"syslog",
]
[[package]]
name = "ff"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d013fc25338cc558c5c2cfbad646908fb23591e2404481826742b651c9af7160"
dependencies = [
"rand_core 0.6.4",
"subtle",
]
[[package]]
name = "figment"
version = "0.10.19"
@ -1934,6 +2049,36 @@ dependencies = [
"phf",
]
[[package]]
name = "group"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5dfbfb3a6cfbd390d5c9564ab283a0349b9b9fcd46a706c1eb10e0db70bfbac7"
dependencies = [
"ff",
"rand_core 0.6.4",
"subtle",
]
[[package]]
name = "h2"
version = "0.3.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8"
dependencies = [
"bytes",
"fnv",
"futures-core",
"futures-sink",
"futures-util",
"http 0.2.12",
"indexmap",
"slab",
"tokio",
"tokio-util",
"tracing",
]
[[package]]
name = "h2"
version = "0.4.10"
@ -2183,6 +2328,7 @@ dependencies = [
"futures-channel",
"futures-core",
"futures-util",
"h2 0.3.26",
"http 0.2.12",
"http-body 0.4.6",
"httparse",
@ -2205,7 +2351,7 @@ dependencies = [
"bytes",
"futures-channel",
"futures-util",
"h2",
"h2 0.4.10",
"http 1.3.1",
"http-body 1.0.1",
"httparse",
@ -2216,6 +2362,22 @@ dependencies = [
"want",
]
[[package]]
name = "hyper-rustls"
version = "0.24.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590"
dependencies = [
"futures-util",
"http 0.2.12",
"hyper 0.14.32",
"log",
"rustls 0.21.12",
"rustls-native-certs 0.6.3",
"tokio",
"tokio-rustls 0.24.1",
]
[[package]]
name = "hyper-rustls"
version = "0.27.6"
@ -2226,7 +2388,7 @@ dependencies = [
"hyper 1.6.0",
"hyper-util",
"rustls 0.23.27",
"rustls-native-certs",
"rustls-native-certs 0.8.1",
"rustls-pki-types",
"tokio",
"tokio-rustls 0.26.2",
@ -3136,6 +3298,17 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]]
name = "p256"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51f44edd08f51e2ade572f141051021c5af22677e42b7dd28a88155151c33594"
dependencies = [
"ecdsa",
"elliptic-curve",
"sha2",
]
[[package]]
name = "parking"
version = "2.2.1"
@ -3387,9 +3560,9 @@ version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f"
dependencies = [
"der",
"pkcs8",
"spki",
"der 0.7.9",
"pkcs8 0.10.2",
"spki 0.7.3",
]
[[package]]
@ -3400,11 +3573,21 @@ checksum = "e847e2c91a18bfa887dd028ec33f2fe6f25db77db3619024764914affe8b69a6"
dependencies = [
"aes",
"cbc",
"der",
"der 0.7.9",
"pbkdf2",
"scrypt",
"sha2",
"spki",
"spki 0.7.3",
]
[[package]]
name = "pkcs8"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba"
dependencies = [
"der 0.6.1",
"spki 0.6.0",
]
[[package]]
@ -3413,10 +3596,10 @@ version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
dependencies = [
"der",
"der 0.7.9",
"pkcs5",
"rand_core 0.6.4",
"spki",
"spki 0.7.3",
]
[[package]]
@ -3853,12 +4036,12 @@ dependencies = [
"futures-channel",
"futures-core",
"futures-util",
"h2",
"h2 0.4.10",
"http 1.3.1",
"http-body 1.0.1",
"http-body-util",
"hyper 1.6.0",
"hyper-rustls",
"hyper-rustls 0.27.6",
"hyper-tls",
"hyper-util",
"ipnet",
@ -3900,6 +4083,17 @@ version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95325155c684b1c89f7765e30bc1c42e4a6da51ca513615660cb8a62ef9a88e3"
[[package]]
name = "rfc6979"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7743f17af12fa0b03b803ba12cd6a8d9483a587e89c69445e3909655c0b9fabb"
dependencies = [
"crypto-bigint 0.4.9",
"hmac",
"zeroize",
]
[[package]]
name = "ring"
version = "0.17.14"
@ -4053,11 +4247,11 @@ dependencies = [
"num-integer",
"num-traits",
"pkcs1",
"pkcs8",
"pkcs8 0.10.2",
"rand_core 0.6.4",
"sha2",
"signature",
"spki",
"signature 2.2.0",
"spki 0.7.3",
"subtle",
"zeroize",
]
@ -4163,6 +4357,18 @@ dependencies = [
"zeroize",
]
[[package]]
name = "rustls-native-certs"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00"
dependencies = [
"openssl-probe",
"rustls-pemfile 1.0.4",
"schannel",
"security-framework 2.11.1",
]
[[package]]
name = "rustls-native-certs"
version = "0.8.1"
@ -4306,6 +4512,20 @@ dependencies = [
"untrusted",
]
[[package]]
name = "sec1"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3be24c1842290c45df0a7bf069e0c268a747ad05a192f2fd7dcfdbc1cba40928"
dependencies = [
"base16ct",
"der 0.6.1",
"generic-array",
"pkcs8 0.9.0",
"subtle",
"zeroize",
]
[[package]]
name = "security-framework"
version = "2.11.1"
@ -4467,6 +4687,16 @@ dependencies = [
"libc",
]
[[package]]
name = "signature"
version = "1.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c"
dependencies = [
"digest",
"rand_core 0.6.4",
]
[[package]]
name = "signature"
version = "2.2.0"
@ -4535,6 +4765,16 @@ dependencies = [
"lock_api",
]
[[package]]
name = "spki"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b"
dependencies = [
"base64ct",
"der 0.6.1",
]
[[package]]
name = "spki"
version = "0.7.3"
@ -4542,7 +4782,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
dependencies = [
"base64ct",
"der",
"der 0.7.9",
]
[[package]]
@ -5195,6 +5435,7 @@ dependencies = [
"argon2",
"aws-config",
"aws-credential-types",
"aws-sdk-sesv2",
"bigdecimal",
"bytes",
"cached",

View file

@ -32,7 +32,9 @@ enable_mimalloc = ["dep:mimalloc"]
# You also need to set an env variable `QUERY_LOGGER=1` to fully activate this so you do not have to re-compile
# if you want to turn off the logging for a specific run.
query_logger = ["dep:diesel_logger"]
aws = ["s3", "ses"]
s3 = ["opendal/services-s3", "dep:aws-config", "dep:aws-credential-types", "dep:anyhow", "dep:reqsign"]
ses = ["dep:aws-config", "dep:aws-sdk-sesv2"]
# Enable unstable features, requires nightly
# Currently only used to enable rusts official ip support
@ -187,6 +189,9 @@ aws-config = { version = "1.6.3", features = ["behavior-version-latest"], option
aws-credential-types = { version = "1.2.3", optional = true }
reqsign = { version = "0.16.3", optional = true }
# AWS Simple Email Service (SES) for sending emails
aws-sdk-sesv2 = { version = "1.81.0", features = ["behavior-version-latest"], optional = true }
# Strip debuginfo from the release builds
# The debug symbols are to provide better panic traces
# Also enable fat LTO and use 1 codegen unit for optimizations

View file

@ -13,6 +13,8 @@ fn main() {
println!("cargo:rustc-cfg=query_logger");
#[cfg(feature = "s3")]
println!("cargo:rustc-cfg=s3");
#[cfg(feature = "ses")]
println!("cargo:rustc-cfg=ses");
#[cfg(not(any(feature = "sqlite", feature = "mysql", feature = "postgresql")))]
compile_error!(
@ -26,6 +28,7 @@ fn main() {
println!("cargo::rustc-check-cfg=cfg(postgresql)");
println!("cargo::rustc-check-cfg=cfg(query_logger)");
println!("cargo::rustc-check-cfg=cfg(s3)");
println!("cargo::rustc-check-cfg=cfg(ses)");
// Rerun when these paths are changed.
// Someone could have checked-out a tag or specific commit, but no other files changed.

View file

@ -746,12 +746,14 @@ make_config! {
smtp_accept_invalid_certs: bool, true, def, false;
/// Accept Invalid Hostnames (Know the risks!) |> DANGEROUS: Allow invalid hostnames. This option introduces significant vulnerabilities to man-in-the-middle attacks!
smtp_accept_invalid_hostnames: bool, true, def, false;
/// Use AWS SES |> Whether to send mail via AWS Simple Email Service (SES)
use_aws_ses: bool, true, def, false;
},
/// Email 2FA Settings
email_2fa: _enable_email_2fa {
/// Enabled |> Disabling will prevent users from setting up new email 2FA and using existing email 2FA configured
_enable_email_2fa: bool, true, auto, |c| c._enable_smtp && (c.smtp_host.is_some() || c.use_sendmail);
_enable_email_2fa: bool, true, auto, |c| c._enable_smtp && (c.smtp_host.is_some() || c.use_sendmail || c.use_aws_ses);
/// Email token size |> Number of digits in an email 2FA token (min: 6, max: 255). Note that the Bitwarden clients are hardcoded to mention 6 digit codes regardless of this setting.
email_token_size: u8, true, def, 6;
/// Token expiration time |> Maximum time in seconds a token is valid. The time the user has to open email client and copy token.
@ -965,6 +967,9 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> {
}
}
}
} else if cfg.use_aws_ses {
#[cfg(not(ses))]
err!("`USE_AWS_SES` is set, but the `ses` feature is not enabled in this build");
} else {
if cfg.smtp_host.is_some() == cfg.smtp_from.is_empty() {
err!("Both `SMTP_HOST` and `SMTP_FROM` need to be set for email support without `USE_SENDMAIL`")
@ -975,7 +980,7 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> {
}
}
if (cfg.smtp_host.is_some() || cfg.use_sendmail) && !is_valid_email(&cfg.smtp_from) {
if (cfg.smtp_host.is_some() || cfg.use_sendmail || cfg.use_aws_ses) && !is_valid_email(&cfg.smtp_from) {
err!(format!("SMTP_FROM '{}' is not a valid email address", cfg.smtp_from))
}
@ -984,7 +989,7 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> {
}
}
if cfg._enable_email_2fa && !(cfg.smtp_host.is_some() || cfg.use_sendmail) {
if cfg._enable_email_2fa && !(cfg.smtp_host.is_some() || cfg.use_sendmail || cfg.use_aws_ses) {
err!("To enable email 2FA, a mail transport must be configured")
}
@ -1186,6 +1191,15 @@ fn opendal_operator_for_path(path: &str) -> Result<opendal::Operator, Error> {
Ok(operator)
}
#[cfg(any(s3, ses))]
pub(crate) async fn aws_sdk_config() -> aws_config::SdkConfig {
use tokio::sync::OnceCell;
static AWS_CONFIG: OnceCell<aws_config::SdkConfig> = OnceCell::const_new();
AWS_CONFIG.get_or_init(aws_config::load_from_env).await.clone()
}
#[cfg(s3)]
fn opendal_s3_operator_for_path(path: &str) -> Result<opendal::Operator, Error> {
// This is a custom AWS credential loader that uses the official AWS Rust
@ -1198,17 +1212,13 @@ fn opendal_s3_operator_for_path(path: &str) -> Result<opendal::Operator, Error>
impl reqsign::AwsCredentialLoad for OpenDALS3CredentialLoader {
async fn load_credential(&self, _client: reqwest::Client) -> anyhow::Result<Option<reqsign::AwsCredential>> {
use aws_credential_types::provider::ProvideCredentials as _;
use tokio::sync::OnceCell;
static DEFAULT_CREDENTIAL_CHAIN: OnceCell<
aws_config::default_provider::credentials::DefaultCredentialsChain,
> = OnceCell::const_new();
let chain = DEFAULT_CREDENTIAL_CHAIN
.get_or_init(|| aws_config::default_provider::credentials::DefaultCredentialsChain::builder().build())
.await;
let creds = chain.provide_credentials().await?;
let creds = aws_sdk_config()
.await
.credentials_provider()
.expect("AWS credentials provider should exist when loading credentials for OpenDAL S3 operator")
.provide_credentials()
.await?;
Ok(Some(reqsign::AwsCredential {
access_key_id: creds.access_key_id().to_string(),
@ -1386,7 +1396,7 @@ impl Config {
}
pub fn mail_enabled(&self) -> bool {
let inner = &self.inner.read().unwrap().config;
inner._enable_smtp && (inner.smtp_host.is_some() || inner.use_sendmail)
inner._enable_smtp && (inner.smtp_host.is_some() || inner.use_sendmail || inner.use_aws_ses)
}
pub async fn get_duo_akey(&self) -> String {

View file

@ -95,6 +95,46 @@ fn smtp_transport() -> AsyncSmtpTransport<Tokio1Executor> {
smtp_client.build()
}
#[cfg(ses)]
async fn send_with_aws_ses(email: Message) -> std::io::Result<()> {
use std::io::Error;
use crate::config::aws_sdk_config;
use aws_sdk_sesv2::{
types::{EmailContent, RawMessage},
Client,
};
use tokio::sync::OnceCell;
static AWS_SESV2_CLIENT: OnceCell<Client> = OnceCell::const_new();
let client = AWS_SESV2_CLIENT
.get_or_init(|| async {
// Initialize the AWS SESv2 client lazily
let config = aws_sdk_config().await;
Client::new(&config)
})
.await;
client
.send_email()
.content(
EmailContent::builder()
.raw(
RawMessage::builder()
.data(email.formatted().into())
.build()
.map_err(|e| Error::other(format!("Failed to build AWS SESv2 RawMessage: {e:#?}")))?,
)
.build(),
)
.send()
.await
.map_err(Error::other)?;
Ok(())
}
// This will sanitize the string values by stripping all the html tags to prevent XSS and HTML Injections
fn sanitize_data(data: &mut serde_json::Value) {
use regex::Regex;
@ -640,6 +680,15 @@ async fn send_with_selected_transport(email: Message) -> EmptyResult {
}
}
}
} else if CONFIG.use_aws_ses() {
#[cfg(ses)]
match send_with_aws_ses(email).await {
Ok(_) => Ok(()),
Err(e) => err!("Failed to send email", format!("Failed to send email using AWS SES: {e:?}")),
}
#[cfg(not(ses))]
err!("Failed to send email", "Failed to send email using AWS SES: `ses` feature is not enabled");
} else {
match smtp_transport().send(email).await {
Ok(_) => Ok(()),