diff --git a/src/auth.rs b/src/auth.rs index 43184369..c082ff09 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -69,14 +69,59 @@ pub async fn initialize_keys() -> Result<(), Error> { Err(e) => return Err(e.into()), }; - let (priv_key, priv_key_buffer) = if let Some(priv_key_buffer) = priv_key_buffer { - (Rsa::private_key_from_pem(priv_key_buffer.to_vec().as_slice())?, priv_key_buffer.to_vec()) + let passphrase = CONFIG.rsa_key_passphrase(); + + let (priv_key, priv_key_buffer) = if let Some(stored_buffer) = priv_key_buffer { + let mut passphrase_error: Option = None; + let mut key_is_unencrypted = true; + + // The callback is only invoked if the key file is encrypted. For unencrypted keys it is never called. + let rsa = Rsa::private_key_from_pem_callback(stored_buffer.to_vec().as_slice(), |buf| { + key_is_unencrypted = false; + if passphrase.is_empty() { + // Only reached when key is encrypted. Return any error to abort the callback; + // the actual error message is stored in passphrase_error above. + passphrase_error = Some(Error::other( + "Private RSA key is encrypted but RSA_KEY_PASSPHRASE is not configured", + )); + return Err(openssl::error::ErrorStack::get()); + } + let bytes = passphrase.as_bytes(); + buf[..bytes.len()].copy_from_slice(bytes); + Ok(bytes.len()) + }); + + let rsa = match rsa { + Ok(r) => r, + Err(_) if passphrase_error.is_some() => return Err(passphrase_error.unwrap().into()), + Err(e) => return Err(e.into()), + }; + + if key_is_unencrypted && !passphrase.is_empty() { + warn!( + "RSA key passphrase is configured but the existing private key '{}' is not encrypted.", + CONFIG.private_rsa_key() + ); + } + + // EncodingKey requires an unencrypted PEM, so export the in-memory key regardless of how it was stored on disk. + let unencrypted_pem = rsa.private_key_to_pem()?; + (rsa, unencrypted_pem) } else { let rsa_key = Rsa::generate(2048)?; - let priv_key_buffer = rsa_key.private_key_to_pem()?; - operator.write(&rsa_key_filename, priv_key_buffer.clone()).await?; + + // Store encrypted on disk if a passphrase is configured, otherwise store plaintext. + let stored_pem = if passphrase.is_empty() { + rsa_key.private_key_to_pem()? + } else { + use openssl::symm::Cipher; + rsa_key.private_key_to_pem_passphrase(Cipher::aes_256_cbc(), passphrase.as_bytes())? + }; + operator.write(&rsa_key_filename, stored_pem).await?; info!("Private key '{}' created correctly", CONFIG.private_rsa_key()); - (rsa_key, priv_key_buffer) + + let unencrypted_pem = rsa_key.private_key_to_pem()?; + (rsa_key, unencrypted_pem) }; let pub_key_buffer = priv_key.public_key_to_pem()?; diff --git a/src/config.rs b/src/config.rs index ae995f69..6faf975b 100644 --- a/src/config.rs +++ b/src/config.rs @@ -520,6 +520,8 @@ make_config! { templates_folder: String, false, auto, |c| format!("{}/templates", c.data_folder); /// Session JWT key rsa_key_filename: String, false, auto, |c| format!("{}/rsa_key", c.data_folder); + /// RSA key passphrase |> Passphrase used to encrypt the private RSA key file on disk (leave empty for unencrypted) + rsa_key_passphrase: String, false, def, String::new(); /// Web vault folder web_vault_folder: String, false, def, "web-vault/".to_string(); }, @@ -959,6 +961,11 @@ fn validate_config(cfg: &ConfigItems, on_update: bool) -> Result<(), Error> { err!(format!("`DATABASE_MIN_CONNS` must be smaller than or equal to `DATABASE_MAX_CONNS`.",)); } + // 1024 = OpenSSL's PEM_BUFSIZE, the size of the buffer passed to the passphrase callback + if cfg.rsa_key_passphrase.as_bytes().len() > 1024 { + err!("`RSA_KEY_PASSPHRASE` must not exceed 1024 bytes"); + } + if let Some(log_file) = &cfg.log_file { if std::fs::OpenOptions::new().append(true).create(true).open(log_file).is_err() { err!("Unable to write to log file", log_file); diff --git a/src/main.rs b/src/main.rs index 60c5a593..dee31a4b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -77,7 +77,7 @@ async fn main() -> Result<(), Error> { check_data_folder().await; auth::initialize_keys().await.unwrap_or_else(|e| { - error!("Error creating private key '{}'\n{e:?}\nExiting Vaultwarden!", CONFIG.private_rsa_key()); + error!("Error creating or loading private key '{}'\n{e:?}\nExiting Vaultwarden!", CONFIG.private_rsa_key()); exit(1); }); check_web_vault();