add slow hashes and password derivation

This commit is contained in:
John Smith
2023-05-16 17:08:15 -04:00
parent 10af290e2f
commit 8660457f95
6 changed files with 226 additions and 5 deletions

View File

@@ -13,6 +13,19 @@ pub trait CryptoSystem {
) -> Result<SharedSecret, VeilidAPIError>;
// Generation
fn random_bytes(&self, len: u32) -> Vec<u8>;
fn default_salt_length(&self) -> u32;
fn hash_password(&self, password: &[u8], salt: &[u8]) -> Result<String, VeilidAPIError>;
fn verify_password(
&self,
password: &[u8],
password_hash: String,
) -> Result<bool, VeilidAPIError>;
fn derive_shared_secret(
&self,
password: &[u8],
salt: &[u8],
) -> Result<SharedSecret, VeilidAPIError>;
fn random_nonce(&self) -> Nonce;
fn random_shared_secret(&self) -> SharedSecret;
fn compute_dh(

View File

@@ -1,7 +1,8 @@
use super::*;
use argon2::password_hash::Salt;
use data_encoding::BASE64URL_NOPAD;
use digest::Digest;
use rand::RngCore;
const AEAD_OVERHEAD: usize = PUBLIC_KEY_LENGTH;
pub const CRYPTO_KIND_NONE: CryptoKind = FourCC([b'N', b'O', b'N', b'E']);
@@ -80,6 +81,52 @@ impl CryptoSystem for CryptoSystemNONE {
}
// Generation
fn random_bytes(&self, len: u32) -> Vec<u8> {
let mut bytes = Vec::<u8>::with_capacity(len as usize);
bytes.resize(len as usize, 0u8);
random_bytes(bytes.as_mut()).unwrap();
bytes
}
fn default_salt_length(&self) -> u32 {
4
}
fn hash_password(&self, password: &[u8], salt: &[u8]) -> Result<String, VeilidAPIError> {
if salt.len() < Salt::MIN_LENGTH || salt.len() > Salt::MAX_LENGTH {
apibail_generic!("invalid salt length");
}
Ok(format!(
"{}:{}",
BASE64URL_NOPAD.encode(salt),
BASE64URL_NOPAD.encode(password)
))
}
fn verify_password(
&self,
password: &[u8],
password_hash: String,
) -> Result<bool, VeilidAPIError> {
let Some((salt, _)) = password_hash.split_once(":") else {
apibail_generic!("invalid format");
};
let Ok(salt) = BASE64URL_NOPAD.decode(salt.as_bytes()) else {
apibail_generic!("invalid salt");
};
return Ok(self.hash_password(password, &salt)? == password_hash);
}
fn derive_shared_secret(
&self,
password: &[u8],
salt: &[u8],
) -> Result<SharedSecret, VeilidAPIError> {
if salt.len() < Salt::MIN_LENGTH || salt.len() > Salt::MAX_LENGTH {
apibail_generic!("invalid salt length");
}
Ok(SharedSecret::new(
*blake3::hash(self.hash_password(password, salt)?.as_bytes()).as_bytes(),
))
}
fn random_nonce(&self) -> Nonce {
let mut nonce = [0u8; NONCE_LENGTH];
random_bytes(&mut nonce).unwrap();

View File

@@ -162,6 +162,66 @@ pub async fn test_dh(vcrypto: CryptoSystemVersion) {
trace!("cached_dh: {:?}", r5);
}
pub async fn test_generation(vcrypto: CryptoSystemVersion) {
let b1 = vcrypto.random_bytes(32);
let b2 = vcrypto.random_bytes(32);
assert_ne!(b1, b2);
assert_eq!(b1.len(), 32);
assert_eq!(b2.len(), 32);
let b3 = vcrypto.random_bytes(0);
let b4 = vcrypto.random_bytes(0);
assert_eq!(b3, b4);
assert_eq!(b3.len(), 0);
assert_ne!(vcrypto.default_salt_length(), 0);
let pstr1 = vcrypto.hash_password(b"abc123", b"qwerasdf").unwrap();
let pstr2 = vcrypto.hash_password(b"abc123", b"qwerasdf").unwrap();
assert_eq!(pstr1, pstr2);
let pstr3 = vcrypto.hash_password(b"abc123", b"qwerasdg").unwrap();
assert_ne!(pstr1, pstr3);
let pstr4 = vcrypto.hash_password(b"abc124", b"qwerasdf").unwrap();
assert_ne!(pstr1, pstr4);
let pstr5 = vcrypto.hash_password(b"abc124", b"qwerasdg").unwrap();
assert_ne!(pstr3, pstr5);
vcrypto
.hash_password(b"abc123", b"qwe")
.expect_err("should reject short salt");
vcrypto
.hash_password(
b"abc123",
b"qwerqwerqwerqwerqwerqwerqwerqwerqwerqwerqwerqwerqwerqwerqwerqwerz",
)
.expect_err("should reject long salt");
assert!(vcrypto.verify_password(b"abc123", pstr1.clone()).unwrap());
assert!(vcrypto.verify_password(b"abc123", pstr2.clone()).unwrap());
assert!(vcrypto.verify_password(b"abc123", pstr3.clone()).unwrap());
assert!(!vcrypto.verify_password(b"abc123", pstr4.clone()).unwrap());
assert!(!vcrypto.verify_password(b"abc123", pstr5.clone()).unwrap());
let ss1 = vcrypto.derive_shared_secret(b"abc123", b"qwerasdf");
let ss2 = vcrypto.derive_shared_secret(b"abc123", b"qwerasdf");
assert_eq!(ss1, ss2);
let ss3 = vcrypto.derive_shared_secret(b"abc123", b"qwerasdg");
assert_ne!(ss1, ss3);
let ss4 = vcrypto.derive_shared_secret(b"abc124", b"qwerasdf");
assert_ne!(ss1, ss4);
let ss5 = vcrypto.derive_shared_secret(b"abc124", b"qwerasdg");
assert_ne!(ss3, ss5);
vcrypto
.derive_shared_secret(b"abc123", b"qwe")
.expect_err("should reject short salt");
vcrypto
.derive_shared_secret(
b"abc123",
b"qwerqwerqwerqwerqwerqwerqwerqwerqwerqwerqwerqwerqwerqwerqwerqwerz",
)
.expect_err("should reject long salt");
}
pub async fn test_all() {
let api = crypto_tests_startup().await;
let crypto = api.crypto().unwrap();
@@ -171,7 +231,8 @@ pub async fn test_all() {
let vcrypto = crypto.get(v).unwrap();
test_aead(vcrypto.clone()).await;
test_no_auth(vcrypto.clone()).await;
test_dh(vcrypto).await;
test_dh(vcrypto.clone()).await;
test_generation(vcrypto).await;
}
crypto_tests_shutdown(api.clone()).await;

View File

@@ -1,5 +1,9 @@
use super::*;
use argon2::{
password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, Salt, SaltString},
Argon2,
};
use chacha20::cipher::{KeyIvInit, StreamCipher};
use chacha20::XChaCha20;
use chacha20poly1305 as ch;
@@ -71,6 +75,63 @@ impl CryptoSystem for CryptoSystemVLD0 {
}
// Generation
fn random_bytes(&self, len: u32) -> Vec<u8> {
let mut bytes = Vec::<u8>::with_capacity(len as usize);
bytes.resize(len as usize, 0u8);
random_bytes(bytes.as_mut()).unwrap();
bytes
}
fn default_salt_length(&self) -> u32 {
16
}
fn hash_password(&self, password: &[u8], salt: &[u8]) -> Result<String, VeilidAPIError> {
if salt.len() < Salt::MIN_LENGTH || salt.len() > Salt::MAX_LENGTH {
apibail_generic!("invalid salt length");
}
// Hash password to PHC string ($argon2id$v=19$...)
let salt = SaltString::encode_b64(salt).map_err(VeilidAPIError::generic)?;
// Argon2 with default params (Argon2id v19)
let argon2 = Argon2::default();
let password_hash = argon2
.hash_password(password, &salt)
.map_err(VeilidAPIError::generic)?
.to_string();
Ok(password_hash)
}
fn verify_password(
&self,
password: &[u8],
password_hash: String,
) -> Result<bool, VeilidAPIError> {
let parsed_hash = PasswordHash::new(&password_hash).map_err(VeilidAPIError::generic)?;
// Argon2 with default params (Argon2id v19)
let argon2 = Argon2::default();
Ok(argon2.verify_password(password, &parsed_hash).is_ok())
}
fn derive_shared_secret(
&self,
password: &[u8],
salt: &[u8],
) -> Result<SharedSecret, VeilidAPIError> {
if salt.len() < Salt::MIN_LENGTH || salt.len() > Salt::MAX_LENGTH {
apibail_generic!("invalid salt length");
}
// Argon2 with default params (Argon2id v19)
let argon2 = Argon2::default();
let mut output_key_material = [0u8; SHARED_SECRET_LENGTH];
argon2
.hash_password_into(password, salt, &mut output_key_material)
.map_err(VeilidAPIError::generic)?;
Ok(SharedSecret::new(output_key_material))
}
fn random_nonce(&self) -> Nonce {
let mut nonce = [0u8; NONCE_LENGTH];
random_bytes(&mut nonce).unwrap();