Add secrecy

This commit is contained in:
Luca Palmieri 2021-12-27 13:24:24 +01:00
parent cb01855668
commit 93ed0c4150
5 changed files with 51 additions and 17 deletions

17
Cargo.lock generated
View file

@ -1762,6 +1762,16 @@ dependencies = [
"untrusted", "untrusted",
] ]
[[package]]
name = "secrecy"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9bd1c54ea06cfd2f6b63219704de0b9b4f72dcc2b8fdef820be6cd799780e91e"
dependencies = [
"serde",
"zeroize",
]
[[package]] [[package]]
name = "semver" name = "semver"
version = "0.9.0" version = "0.9.0"
@ -2746,6 +2756,7 @@ dependencies = [
"quickcheck_macros", "quickcheck_macros",
"rand 0.8.4", "rand 0.8.4",
"reqwest", "reqwest",
"secrecy",
"serde", "serde",
"serde-aux", "serde-aux",
"serde_json", "serde_json",
@ -2763,6 +2774,12 @@ dependencies = [
"wiremock", "wiremock",
] ]
[[package]]
name = "zeroize"
version = "1.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d68d9dcec5f9b43a30d38c49f91dfedfaac384cb8f085faca366c26207dd1619"
[[package]] [[package]]
name = "zstd" name = "zstd"
version = "0.9.1+zstd.1.5.1" version = "0.9.1+zstd.1.5.1"

View file

@ -36,6 +36,7 @@ tracing-actix-web = "0.5.0-beta.6"
anyhow = "1.0.40" anyhow = "1.0.40"
base64 = "0.13.0" base64 = "0.13.0"
argon2 = { version = "0.3", features = ["std"] } argon2 = { version = "0.3", features = ["std"] }
secrecy = { version = "0.8", features = ["serde"] }
[dev-dependencies] [dev-dependencies]
once_cell = "1.7.2" once_cell = "1.7.2"

View file

@ -1,4 +1,5 @@
use crate::domain::SubscriberEmail; use crate::domain::SubscriberEmail;
use secrecy::{ExposeSecret, Secret};
use serde_aux::field_attributes::deserialize_number_from_string; use serde_aux::field_attributes::deserialize_number_from_string;
use sqlx::postgres::{PgConnectOptions, PgSslMode}; use sqlx::postgres::{PgConnectOptions, PgSslMode};
use sqlx::ConnectOptions; use sqlx::ConnectOptions;
@ -22,7 +23,7 @@ pub struct ApplicationSettings {
#[derive(serde::Deserialize, Clone)] #[derive(serde::Deserialize, Clone)]
pub struct DatabaseSettings { pub struct DatabaseSettings {
pub username: String, pub username: String,
pub password: String, pub password: Secret<String>,
#[serde(deserialize_with = "deserialize_number_from_string")] #[serde(deserialize_with = "deserialize_number_from_string")]
pub port: u16, pub port: u16,
pub host: String, pub host: String,
@ -40,7 +41,7 @@ impl DatabaseSettings {
PgConnectOptions::new() PgConnectOptions::new()
.host(&self.host) .host(&self.host)
.username(&self.username) .username(&self.username)
.password(&self.password) .password(&self.password.expose_secret())
.port(self.port) .port(self.port)
.ssl_mode(ssl_mode) .ssl_mode(ssl_mode)
} }
@ -56,7 +57,8 @@ impl DatabaseSettings {
pub struct EmailClientSettings { pub struct EmailClientSettings {
pub base_url: String, pub base_url: String,
pub sender_email: String, pub sender_email: String,
pub authorization_token: String, pub authorization_token: Secret<String>,
#[serde(deserialize_with = "deserialize_number_from_string")]
pub timeout_milliseconds: u64, pub timeout_milliseconds: u64,
} }

View file

@ -1,18 +1,19 @@
use crate::domain::SubscriberEmail; use crate::domain::SubscriberEmail;
use reqwest::Client; use reqwest::Client;
use secrecy::{ExposeSecret, Secret};
pub struct EmailClient { pub struct EmailClient {
http_client: Client, http_client: Client,
base_url: String, base_url: String,
sender: SubscriberEmail, sender: SubscriberEmail,
authorization_token: String, authorization_token: Secret<String>,
} }
impl EmailClient { impl EmailClient {
pub fn new( pub fn new(
base_url: String, base_url: String,
sender: SubscriberEmail, sender: SubscriberEmail,
authorization_token: String, authorization_token: Secret<String>,
timeout: std::time::Duration, timeout: std::time::Duration,
) -> Self { ) -> Self {
let http_client = Client::builder().timeout(timeout).build().unwrap(); let http_client = Client::builder().timeout(timeout).build().unwrap();
@ -41,7 +42,10 @@ impl EmailClient {
}; };
self.http_client self.http_client
.post(&url) .post(&url)
.header("X-Postmark-Server-Token", &self.authorization_token) .header(
"X-Postmark-Server-Token",
self.authorization_token.expose_secret(),
)
.json(&request_body) .json(&request_body)
.send() .send()
.await? .await?
@ -68,6 +72,7 @@ mod tests {
use fake::faker::internet::en::SafeEmail; use fake::faker::internet::en::SafeEmail;
use fake::faker::lorem::en::{Paragraph, Sentence}; use fake::faker::lorem::en::{Paragraph, Sentence};
use fake::{Fake, Faker}; use fake::{Fake, Faker};
use secrecy::Secret;
use wiremock::matchers::{any, header, header_exists, method, path}; use wiremock::matchers::{any, header, header_exists, method, path};
use wiremock::{Mock, MockServer, Request, ResponseTemplate}; use wiremock::{Mock, MockServer, Request, ResponseTemplate};
@ -108,7 +113,7 @@ mod tests {
EmailClient::new( EmailClient::new(
base_url, base_url,
email(), email(),
Faker.fake(), Secret::new(Faker.fake()),
std::time::Duration::from_millis(200), std::time::Duration::from_millis(200),
) )
} }

View file

@ -9,6 +9,7 @@ use actix_web::http::{
use actix_web::{web, HttpResponse, ResponseError}; use actix_web::{web, HttpResponse, ResponseError};
use anyhow::Context; use anyhow::Context;
use argon2::{Argon2, PasswordHash, PasswordVerifier}; use argon2::{Argon2, PasswordHash, PasswordVerifier};
use secrecy::{ExposeSecret, Secret};
use sqlx::PgPool; use sqlx::PgPool;
#[derive(serde::Deserialize)] #[derive(serde::Deserialize)]
@ -57,7 +58,7 @@ impl ResponseError for PublishError {
struct Credentials { struct Credentials {
username: String, username: String,
password: String, password: Secret<String>,
} }
fn basic_authentication(headers: &HeaderMap) -> Result<Credentials, anyhow::Error> { fn basic_authentication(headers: &HeaderMap) -> Result<Credentials, anyhow::Error> {
@ -85,14 +86,17 @@ fn basic_authentication(headers: &HeaderMap) -> Result<Credentials, anyhow::Erro
.ok_or_else(|| anyhow::anyhow!("A password must be provided in 'Basic' auth."))? .ok_or_else(|| anyhow::anyhow!("A password must be provided in 'Basic' auth."))?
.to_string(); .to_string();
Ok(Credentials { username, password }) Ok(Credentials {
username,
password: Secret::new(password),
})
} }
#[tracing::instrument(name = "Get stored credentials", skip(username, pool))] #[tracing::instrument(name = "Get stored credentials", skip(username, pool))]
async fn get_stored_credentials( async fn get_stored_credentials(
username: &str, username: &str,
pool: &PgPool, pool: &PgPool,
) -> Result<Option<(uuid::Uuid, String)>, anyhow::Error> { ) -> Result<Option<(uuid::Uuid, Secret<String>)>, anyhow::Error> {
let row = sqlx::query!( let row = sqlx::query!(
r#" r#"
SELECT user_id, password_hash SELECT user_id, password_hash
@ -104,7 +108,7 @@ async fn get_stored_credentials(
.fetch_optional(pool) .fetch_optional(pool)
.await .await
.context("Failed to performed a query to retrieve stored credentials.")? .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) Ok(row)
} }
@ -114,10 +118,12 @@ async fn validate_credentials(
pool: &PgPool, pool: &PgPool,
) -> Result<uuid::Uuid, PublishError> { ) -> Result<uuid::Uuid, PublishError> {
let mut user_id = None; 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$\ gZiV/M1gPc22ElAH/Jh1Hw$\
CWOrkoo7oJBQ/iyh7uJ0LO2aLEfrHwTWllSAxT0zRno" CWOrkoo7oJBQ/iyh7uJ0LO2aLEfrHwTWllSAxT0zRno"
.to_string(); .to_string(),
);
if let Some((stored_user_id, stored_password_hash)) = if let Some((stored_user_id, stored_password_hash)) =
get_stored_credentials(&credentials.username, pool) get_stored_credentials(&credentials.username, pool)
@ -143,15 +149,18 @@ async fn validate_credentials(
skip(expected_password_hash, password_candidate) skip(expected_password_hash, password_candidate)
)] )]
fn verify_password_hash( fn verify_password_hash(
expected_password_hash: String, expected_password_hash: Secret<String>,
password_candidate: String, password_candidate: Secret<String>,
) -> Result<(), PublishError> { ) -> 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.") .context("Failed to parse hash in PHC string format.")
.map_err(PublishError::UnexpectedError)?; .map_err(PublishError::UnexpectedError)?;
Argon2::default() 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.") .context("Invalid password.")
.map_err(PublishError::AuthError) .map_err(PublishError::AuthError)
} }