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

38
Cargo.lock generated
View File

@ -175,6 +175,17 @@ version = "1.0.70"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7de8ce5e0f9f8d88245311066a578d72b7af3e7088f32783804676302df237e4" checksum = "7de8ce5e0f9f8d88245311066a578d72b7af3e7088f32783804676302df237e4"
[[package]]
name = "argon2"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95c2fcf79ad1932ac6269a738109997a83c227c09b75842ae564dc8ede6a861c"
dependencies = [
"base64ct",
"blake2",
"password-hash",
]
[[package]] [[package]]
name = "arraydeque" name = "arraydeque"
version = "0.4.5" version = "0.4.5"
@ -595,6 +606,12 @@ version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a" checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a"
[[package]]
name = "base64ct"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
[[package]] [[package]]
name = "bindgen" name = "bindgen"
version = "0.57.0" version = "0.57.0"
@ -661,6 +678,15 @@ dependencies = [
"wyz", "wyz",
] ]
[[package]]
name = "blake2"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe"
dependencies = [
"digest 0.10.6",
]
[[package]] [[package]]
name = "blake3" name = "blake3"
version = "1.3.3" version = "1.3.3"
@ -3809,6 +3835,17 @@ dependencies = [
"windows-sys 0.45.0", "windows-sys 0.45.0",
] ]
[[package]]
name = "password-hash"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166"
dependencies = [
"base64ct",
"rand_core 0.6.4",
"subtle",
]
[[package]] [[package]]
name = "paste" name = "paste"
version = "1.0.12" version = "1.0.12"
@ -6030,6 +6067,7 @@ dependencies = [
name = "veilid-core" name = "veilid-core"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"argon2",
"async-io", "async-io",
"async-lock", "async-lock",
"async-std", "async-std",

View File

@ -10,9 +10,9 @@ license = "LGPL-2.0-or-later OR MPL-2.0 OR (MIT AND BSD-3-Clause)"
crate-type = ["cdylib", "staticlib", "rlib"] crate-type = ["cdylib", "staticlib", "rlib"]
[features] [features]
default = [ "enable-crypto-vld0" ] default = ["enable-crypto-vld0"]
crypto-test = [ "enable-crypto-vld0", "enable-crypto-none" ] crypto-test = ["enable-crypto-vld0", "enable-crypto-none"]
crypto-test-none = [ "enable-crypto-none" ] crypto-test-none = ["enable-crypto-none"]
enable-crypto-vld0 = [] enable-crypto-vld0 = []
enable-crypto-none = [] enable-crypto-none = []
rt-async-std = ["async-std", "async-std-resolver", "async_executors/async_std", "rtnetlink?/smol_socket", "veilid-tools/rt-async-std"] rt-async-std = ["async-std", "async-std-resolver", "async_executors/async_std", "rtnetlink?/smol_socket", "veilid-tools/rt-async-std"]
@ -70,6 +70,7 @@ rkyv = { version = "^0", default_features = false, features = ["std", "alloc", "
data-encoding = { version = "^2" } data-encoding = { version = "^2" }
weak-table = "0.3.2" weak-table = "0.3.2"
range-set-blaze = { git = "https://github.com/crioux/range-set-blaze.git" } # "0.1.4" xxx replace with git repo range-set-blaze = { git = "https://github.com/crioux/range-set-blaze.git" } # "0.1.4" xxx replace with git repo
argon2 = "0.5.0"
# Dependencies for native builds only # Dependencies for native builds only
# Linux, Windows, Mac, iOS, Android # Linux, Windows, Mac, iOS, Android

View File

@ -13,6 +13,19 @@ pub trait CryptoSystem {
) -> Result<SharedSecret, VeilidAPIError>; ) -> Result<SharedSecret, VeilidAPIError>;
// Generation // 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_nonce(&self) -> Nonce;
fn random_shared_secret(&self) -> SharedSecret; fn random_shared_secret(&self) -> SharedSecret;
fn compute_dh( fn compute_dh(

View File

@ -1,7 +1,8 @@
use super::*; use super::*;
use argon2::password_hash::Salt;
use data_encoding::BASE64URL_NOPAD;
use digest::Digest; use digest::Digest;
use rand::RngCore; use rand::RngCore;
const AEAD_OVERHEAD: usize = PUBLIC_KEY_LENGTH; const AEAD_OVERHEAD: usize = PUBLIC_KEY_LENGTH;
pub const CRYPTO_KIND_NONE: CryptoKind = FourCC([b'N', b'O', b'N', b'E']); pub const CRYPTO_KIND_NONE: CryptoKind = FourCC([b'N', b'O', b'N', b'E']);
@ -80,6 +81,52 @@ impl CryptoSystem for CryptoSystemNONE {
} }
// Generation // 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 { fn random_nonce(&self) -> Nonce {
let mut nonce = [0u8; NONCE_LENGTH]; let mut nonce = [0u8; NONCE_LENGTH];
random_bytes(&mut nonce).unwrap(); random_bytes(&mut nonce).unwrap();

View File

@ -162,6 +162,66 @@ pub async fn test_dh(vcrypto: CryptoSystemVersion) {
trace!("cached_dh: {:?}", r5); 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() { pub async fn test_all() {
let api = crypto_tests_startup().await; let api = crypto_tests_startup().await;
let crypto = api.crypto().unwrap(); let crypto = api.crypto().unwrap();
@ -171,7 +231,8 @@ pub async fn test_all() {
let vcrypto = crypto.get(v).unwrap(); let vcrypto = crypto.get(v).unwrap();
test_aead(vcrypto.clone()).await; test_aead(vcrypto.clone()).await;
test_no_auth(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; crypto_tests_shutdown(api.clone()).await;

View File

@ -1,5 +1,9 @@
use super::*; use super::*;
use argon2::{
password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, Salt, SaltString},
Argon2,
};
use chacha20::cipher::{KeyIvInit, StreamCipher}; use chacha20::cipher::{KeyIvInit, StreamCipher};
use chacha20::XChaCha20; use chacha20::XChaCha20;
use chacha20poly1305 as ch; use chacha20poly1305 as ch;
@ -71,6 +75,63 @@ impl CryptoSystem for CryptoSystemVLD0 {
} }
// Generation // 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 { fn random_nonce(&self) -> Nonce {
let mut nonce = [0u8; NONCE_LENGTH]; let mut nonce = [0u8; NONCE_LENGTH];
random_bytes(&mut nonce).unwrap(); random_bytes(&mut nonce).unwrap();