diff --git a/Cargo.lock b/Cargo.lock index 31c87f8f..142cc8d4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -175,6 +175,17 @@ version = "1.0.70" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "arraydeque" version = "0.4.5" @@ -595,6 +606,12 @@ version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a" +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + [[package]] name = "bindgen" version = "0.57.0" @@ -661,6 +678,15 @@ dependencies = [ "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]] name = "blake3" version = "1.3.3" @@ -3809,6 +3835,17 @@ dependencies = [ "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]] name = "paste" version = "1.0.12" @@ -6030,6 +6067,7 @@ dependencies = [ name = "veilid-core" version = "0.1.0" dependencies = [ + "argon2", "async-io", "async-lock", "async-std", diff --git a/veilid-core/Cargo.toml b/veilid-core/Cargo.toml index 29d0ea05..10a053ec 100644 --- a/veilid-core/Cargo.toml +++ b/veilid-core/Cargo.toml @@ -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"] [features] -default = [ "enable-crypto-vld0" ] -crypto-test = [ "enable-crypto-vld0", "enable-crypto-none" ] -crypto-test-none = [ "enable-crypto-none" ] +default = ["enable-crypto-vld0"] +crypto-test = ["enable-crypto-vld0", "enable-crypto-none"] +crypto-test-none = ["enable-crypto-none"] enable-crypto-vld0 = [] enable-crypto-none = [] 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" } 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 +argon2 = "0.5.0" # Dependencies for native builds only # Linux, Windows, Mac, iOS, Android diff --git a/veilid-core/src/crypto/crypto_system.rs b/veilid-core/src/crypto/crypto_system.rs index 84ac60a6..b8b30dc1 100644 --- a/veilid-core/src/crypto/crypto_system.rs +++ b/veilid-core/src/crypto/crypto_system.rs @@ -13,6 +13,19 @@ pub trait CryptoSystem { ) -> Result; // Generation + fn random_bytes(&self, len: u32) -> Vec; + fn default_salt_length(&self) -> u32; + fn hash_password(&self, password: &[u8], salt: &[u8]) -> Result; + fn verify_password( + &self, + password: &[u8], + password_hash: String, + ) -> Result; + fn derive_shared_secret( + &self, + password: &[u8], + salt: &[u8], + ) -> Result; fn random_nonce(&self) -> Nonce; fn random_shared_secret(&self) -> SharedSecret; fn compute_dh( diff --git a/veilid-core/src/crypto/none/mod.rs b/veilid-core/src/crypto/none/mod.rs index f59944ea..18af6b3a 100644 --- a/veilid-core/src/crypto/none/mod.rs +++ b/veilid-core/src/crypto/none/mod.rs @@ -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 { + let mut bytes = Vec::::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 { + 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 { + 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 { + 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(); diff --git a/veilid-core/src/crypto/tests/test_crypto.rs b/veilid-core/src/crypto/tests/test_crypto.rs index 3f236b2e..469e3689 100644 --- a/veilid-core/src/crypto/tests/test_crypto.rs +++ b/veilid-core/src/crypto/tests/test_crypto.rs @@ -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; diff --git a/veilid-core/src/crypto/vld0/mod.rs b/veilid-core/src/crypto/vld0/mod.rs index cebe9a2b..04e4c5f6 100644 --- a/veilid-core/src/crypto/vld0/mod.rs +++ b/veilid-core/src/crypto/vld0/mod.rs @@ -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 { + let mut bytes = Vec::::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 { + 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 { + 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 { + 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();