diff --git a/Cargo.lock b/Cargo.lock index 48916c0..13eb46d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1762,6 +1762,16 @@ dependencies = [ "untrusted", ] +[[package]] +name = "secrecy" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bd1c54ea06cfd2f6b63219704de0b9b4f72dcc2b8fdef820be6cd799780e91e" +dependencies = [ + "serde", + "zeroize", +] + [[package]] name = "semver" version = "0.9.0" @@ -2746,6 +2756,7 @@ dependencies = [ "quickcheck_macros", "rand 0.8.4", "reqwest", + "secrecy", "serde", "serde-aux", "serde_json", @@ -2763,6 +2774,12 @@ dependencies = [ "wiremock", ] +[[package]] +name = "zeroize" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d68d9dcec5f9b43a30d38c49f91dfedfaac384cb8f085faca366c26207dd1619" + [[package]] name = "zstd" version = "0.9.1+zstd.1.5.1" diff --git a/Cargo.toml b/Cargo.toml index 544e3d8..ccb928e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,7 @@ tracing-actix-web = "0.5.0-beta.6" anyhow = "1.0.40" base64 = "0.13.0" argon2 = { version = "0.3", features = ["std"] } +secrecy = { version = "0.8", features = ["serde"] } [dev-dependencies] once_cell = "1.7.2" diff --git a/src/configuration.rs b/src/configuration.rs index aa036b0..e450646 100644 --- a/src/configuration.rs +++ b/src/configuration.rs @@ -1,4 +1,5 @@ use crate::domain::SubscriberEmail; +use secrecy::{ExposeSecret, Secret}; use serde_aux::field_attributes::deserialize_number_from_string; use sqlx::postgres::{PgConnectOptions, PgSslMode}; use sqlx::ConnectOptions; @@ -22,7 +23,7 @@ pub struct ApplicationSettings { #[derive(serde::Deserialize, Clone)] pub struct DatabaseSettings { pub username: String, - pub password: String, + pub password: Secret, #[serde(deserialize_with = "deserialize_number_from_string")] pub port: u16, pub host: String, @@ -40,7 +41,7 @@ impl DatabaseSettings { PgConnectOptions::new() .host(&self.host) .username(&self.username) - .password(&self.password) + .password(&self.password.expose_secret()) .port(self.port) .ssl_mode(ssl_mode) } @@ -56,7 +57,8 @@ impl DatabaseSettings { pub struct EmailClientSettings { pub base_url: String, pub sender_email: String, - pub authorization_token: String, + pub authorization_token: Secret, + #[serde(deserialize_with = "deserialize_number_from_string")] pub timeout_milliseconds: u64, } diff --git a/src/email_client.rs b/src/email_client.rs index 64197f5..03a6153 100644 --- a/src/email_client.rs +++ b/src/email_client.rs @@ -1,18 +1,19 @@ use crate::domain::SubscriberEmail; use reqwest::Client; +use secrecy::{ExposeSecret, Secret}; pub struct EmailClient { http_client: Client, base_url: String, sender: SubscriberEmail, - authorization_token: String, + authorization_token: Secret, } impl EmailClient { pub fn new( base_url: String, sender: SubscriberEmail, - authorization_token: String, + authorization_token: Secret, timeout: std::time::Duration, ) -> Self { let http_client = Client::builder().timeout(timeout).build().unwrap(); @@ -41,7 +42,10 @@ impl EmailClient { }; self.http_client .post(&url) - .header("X-Postmark-Server-Token", &self.authorization_token) + .header( + "X-Postmark-Server-Token", + self.authorization_token.expose_secret(), + ) .json(&request_body) .send() .await? @@ -68,6 +72,7 @@ mod tests { use fake::faker::internet::en::SafeEmail; use fake::faker::lorem::en::{Paragraph, Sentence}; use fake::{Fake, Faker}; + use secrecy::Secret; use wiremock::matchers::{any, header, header_exists, method, path}; use wiremock::{Mock, MockServer, Request, ResponseTemplate}; @@ -108,7 +113,7 @@ mod tests { EmailClient::new( base_url, email(), - Faker.fake(), + Secret::new(Faker.fake()), std::time::Duration::from_millis(200), ) } diff --git a/src/routes/newsletters.rs b/src/routes/newsletters.rs index e327bd5..a5332cf 100644 --- a/src/routes/newsletters.rs +++ b/src/routes/newsletters.rs @@ -9,6 +9,7 @@ use actix_web::http::{ use actix_web::{web, HttpResponse, ResponseError}; use anyhow::Context; use argon2::{Argon2, PasswordHash, PasswordVerifier}; +use secrecy::{ExposeSecret, Secret}; use sqlx::PgPool; #[derive(serde::Deserialize)] @@ -57,7 +58,7 @@ impl ResponseError for PublishError { struct Credentials { username: String, - password: String, + password: Secret, } fn basic_authentication(headers: &HeaderMap) -> Result { @@ -85,14 +86,17 @@ fn basic_authentication(headers: &HeaderMap) -> Result Result, anyhow::Error> { +) -> Result)>, anyhow::Error> { let row = sqlx::query!( r#" SELECT user_id, password_hash @@ -104,7 +108,7 @@ async fn get_stored_credentials( .fetch_optional(pool) .await .context("Failed to performed a query to retrieve stored credentials.")? - .map(|row| (row.user_id, row.password_hash)); + .map(|row| (row.user_id, Secret::new(row.password_hash))); Ok(row) } @@ -114,10 +118,12 @@ async fn validate_credentials( pool: &PgPool, ) -> Result { let mut user_id = None; - let mut expected_password_hash = "$argon2id$v=19$m=15000,t=2,p=1$\ + let mut expected_password_hash = Secret::new( + "$argon2id$v=19$m=15000,t=2,p=1$\ gZiV/M1gPc22ElAH/Jh1Hw$\ CWOrkoo7oJBQ/iyh7uJ0LO2aLEfrHwTWllSAxT0zRno" - .to_string(); + .to_string(), + ); if let Some((stored_user_id, stored_password_hash)) = get_stored_credentials(&credentials.username, pool) @@ -143,15 +149,18 @@ async fn validate_credentials( skip(expected_password_hash, password_candidate) )] fn verify_password_hash( - expected_password_hash: String, - password_candidate: String, + expected_password_hash: Secret, + password_candidate: Secret, ) -> Result<(), PublishError> { - let expected_password_hash = PasswordHash::new(&expected_password_hash) + let expected_password_hash = PasswordHash::new(expected_password_hash.expose_secret()) .context("Failed to parse hash in PHC string format.") .map_err(PublishError::UnexpectedError)?; Argon2::default() - .verify_password(password_candidate.as_bytes(), &expected_password_hash) + .verify_password( + password_candidate.expose_secret().as_bytes(), + &expected_password_hash, + ) .context("Invalid password.") .map_err(PublishError::AuthError) }