Last active
January 5, 2024 21:04
-
-
Save OneOfOne/0d93d950f69fa256ec3047acbc7be684 to your computer and use it in GitHub Desktop.
hashed password in rust
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
use base64::{ | |
engine::{general_purpose, GeneralPurpose}, | |
Engine as _, | |
}; | |
use rand::{thread_rng, Rng as _}; | |
use sha2::{Digest as _, Sha256}; | |
const B64: GeneralPurpose = general_purpose::URL_SAFE_NO_PAD; | |
#[derive(Debug, Clone, Copy, PartialEq, Eq)] | |
pub struct SaltyPassword { | |
pub pass: [u8; 32], | |
pub salt: [u8; 8], | |
pub cost: u8, | |
} | |
impl SaltyPassword { | |
pub fn new(pass: impl AsRef<str>, cost: u8) -> Self { | |
let mut salt = [0u8; 8]; | |
thread_rng().fill(&mut salt[..]); | |
Self::new_with_salt(pass, salt, cost) | |
} | |
pub fn new_with_salt(pass: impl AsRef<str>, salt: [u8; 8], cost: u8) -> Self { | |
let pass = pass.as_ref().as_bytes(); | |
let mut h = Sha256::new(); | |
h.update(&salt); | |
h.update(&pass); | |
for _ in 0..cost { | |
h.update(&salt); | |
h.update(&pass); | |
} | |
Self { | |
pass: h.finalize().into(), | |
salt, | |
cost, | |
} | |
} | |
pub fn from_hashed(hashed: impl AsRef<str>) -> Result<Self, &'static str> { | |
let hashed = hashed.as_ref(); | |
let parts = hashed.split("$").collect::<Vec<&str>>(); | |
if parts.len() != 3 { | |
return Err("invalid hashed password"); | |
} | |
let cost = parts[0].parse::<u8>().or(Err("invalid cost"))?; | |
let salt = B64.decode(parts[1]).or(Err("invalid salt bytes"))?; | |
let salt: [u8; 8] = salt.try_into().or(Err("invalid salt"))?; | |
let pass = B64.decode(parts[2]).or(Err("invalid pass bytes"))?; | |
let pass: [u8; 32] = pass.try_into().or(Err("invalid pass"))?; | |
Ok(Self { pass, salt, cost }) | |
} | |
pub fn salt_b64(self) -> String { | |
B64.encode(self.salt) | |
} | |
pub fn pass_b64(self) -> String { | |
B64.encode(self.pass) | |
} | |
pub fn as_string(self) -> String { | |
format!("{}${}${}", self.cost, self.salt_b64(), self.pass_b64()) | |
} | |
pub fn validate( | |
hashed: impl AsRef<str>, | |
password: impl AsRef<str>, | |
) -> Result<bool, &'static str> { | |
let p1 = Self::from_hashed(hashed)?; | |
let p2 = Self::new_with_salt(password, p1.salt, p1.cost); | |
Ok(p1 == p2) | |
} | |
} | |
impl PartialEq<&str> for SaltyPassword { | |
fn eq(&self, pass: &&str) -> bool { | |
let other = Self::new_with_salt(pass, self.salt, self.cost); | |
self == &other | |
} | |
} | |
impl PartialEq<String> for SaltyPassword { | |
fn eq(&self, pass: &String) -> bool { | |
self == &pass.as_str() | |
} | |
} | |
#[cfg(test)] | |
mod tests { | |
use super::*; | |
#[test] | |
fn hashed_password() { | |
let pass = SaltyPassword::new("password", 10); | |
assert!(pass == "password"); | |
assert!(pass != "wrong password"); | |
} | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
test tests::hashed_password ... ok