From 8af226cd057bd50ba034b956e70d6e420aba7bf2 Mon Sep 17 00:00:00 2001
From: Philippe Loctaux
Date: Sun, 5 Mar 2023 23:28:14 +0100
Subject: [PATCH] hash: hash crate for all hashing needs, password
---
crates/hash/Cargo.toml | 9 ++++
crates/hash/src/error.rs | 11 +++++
crates/hash/src/hash.rs | 82 +++++++++++++++++++++++++++++++++++++
crates/hash/src/lib.rs | 6 +++
crates/hash/src/password.rs | 20 +++++++++
5 files changed, 128 insertions(+)
create mode 100644 crates/hash/Cargo.toml
create mode 100644 crates/hash/src/error.rs
create mode 100644 crates/hash/src/hash.rs
create mode 100644 crates/hash/src/lib.rs
create mode 100644 crates/hash/src/password.rs
diff --git a/crates/hash/Cargo.toml b/crates/hash/Cargo.toml
new file mode 100644
index 0000000..55b6ef1
--- /dev/null
+++ b/crates/hash/Cargo.toml
@@ -0,0 +1,9 @@
+[package]
+name = "hash"
+version = "0.0.0"
+edition = "2021"
+
+[dependencies]
+argon2 = { version = "0.4.1" }
+rand_core = { version = "0.6.4", features = ["std"] }
+thiserror = { workspace = true }
diff --git a/crates/hash/src/error.rs b/crates/hash/src/error.rs
new file mode 100644
index 0000000..9345594
--- /dev/null
+++ b/crates/hash/src/error.rs
@@ -0,0 +1,11 @@
+// error
+#[derive(thiserror::Error)]
+// the rest
+#[derive(Debug)]
+pub enum Error {
+ #[error("Failed to hash value")]
+ Hash,
+
+ #[error("Failed to parse hashed value")]
+ Parse,
+}
diff --git a/crates/hash/src/hash.rs b/crates/hash/src/hash.rs
new file mode 100644
index 0000000..bdd6cf4
--- /dev/null
+++ b/crates/hash/src/hash.rs
@@ -0,0 +1,82 @@
+use crate::error::Error;
+use argon2::{
+ password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
+ Argon2,
+};
+
+pub(crate) fn hash(value: &str) -> Result {
+ let salt = SaltString::generate(&mut OsRng);
+
+ // Argon2 with default params (Argon2id v19)
+ let argon2 = Argon2::default();
+
+ // Hash value to PHC string ($argon2id$v=19$...)
+ argon2
+ .hash_password(value.as_bytes(), &salt)
+ .map(|hashed| hashed.to_string())
+ .map_err(|_| Error::Hash)
+}
+
+fn verify(value: &str, hashed_value: &str) -> Result {
+ // Verify value against PHC string.
+ //
+ // NOTE: hash params from `parsed_hash` are used instead of what is configured in the
+ // `Argon2` instance.
+ let parsed_hash = PasswordHash::new(hashed_value).map_err(|_| Error::Parse)?;
+
+ let hashes_match = Argon2::default()
+ .verify_password(value.as_bytes(), &parsed_hash)
+ .is_ok();
+
+ Ok(hashes_match)
+}
+
+#[derive(Debug)]
+pub struct Hash {
+ plain: Option,
+ hash: String,
+}
+
+impl Hash {
+ pub fn from_plain(plain: impl Into) -> Result {
+ let plain = plain.into();
+ let hash = hash(&plain)?;
+ Ok(Self {
+ plain: Some(plain),
+ hash,
+ })
+ }
+ pub fn from_hash(hash: impl Into) -> Self {
+ Self {
+ plain: None,
+ hash: hash.into(),
+ }
+ }
+ pub fn plain(&self) -> Option<&str> {
+ self.plain.as_deref()
+ }
+ pub fn hash(&self) -> &str {
+ self.hash.as_ref()
+ }
+ pub fn compare(&self, plain: &str) -> Result {
+ verify(plain, self.hash())
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use crate::hash::Hash;
+
+ #[test]
+ fn can_be_compared_with_hashed() {
+ let value = Hash::from_plain("Testing the hashing").unwrap();
+
+ // Create from hashed value
+ let value_from_hash = Hash::from_hash(value.hash());
+
+ // Make sure when hashing the plain text version, it can match the hashed one
+ let does_value_match = value_from_hash.compare(value.plain().unwrap()).unwrap();
+
+ assert!(does_value_match);
+ }
+}
diff --git a/crates/hash/src/lib.rs b/crates/hash/src/lib.rs
new file mode 100644
index 0000000..769d4c5
--- /dev/null
+++ b/crates/hash/src/lib.rs
@@ -0,0 +1,6 @@
+mod error;
+mod hash;
+mod password;
+
+pub use error::Error;
+pub use password::Password;
diff --git a/crates/hash/src/password.rs b/crates/hash/src/password.rs
new file mode 100644
index 0000000..398272f
--- /dev/null
+++ b/crates/hash/src/password.rs
@@ -0,0 +1,20 @@
+use crate::error::Error;
+use crate::hash::{hash, Hash};
+
+#[derive(Debug)]
+pub struct Password(Hash);
+
+impl Password {
+ pub fn new(plain: &str) -> Result {
+ Ok(Self(Hash::from_hash(hash(plain)?)))
+ }
+ pub fn from_hash(hash: impl Into) -> Self {
+ Self(Hash::from_hash(hash))
+ }
+ pub fn hash(&self) -> &str {
+ self.0.hash()
+ }
+ pub fn compare(&self, plain: &str) -> Result {
+ self.0.compare(plain).map_err(Error::from)
+ }
+}