mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 12:37:00 +01:00
Add support for argon2id (#1736)
This commit is contained in:
parent
e61c9bdd0d
commit
c65be8174a
39
Cargo.lock
generated
39
Cargo.lock
generated
|
@ -132,6 +132,17 @@ version = "0.13.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d301b3b94cb4b2f23d7917810addbbaff90738e0ca2be692bd027e70d7e0330c"
|
||||
|
||||
[[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 = "arrayref"
|
||||
version = "0.3.7"
|
||||
|
@ -506,6 +517,12 @@ version = "0.21.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d"
|
||||
|
||||
[[package]]
|
||||
name = "base64ct"
|
||||
version = "1.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
|
||||
|
||||
[[package]]
|
||||
name = "base64urlsafedata"
|
||||
version = "0.1.3"
|
||||
|
@ -576,6 +593,15 @@ version = "2.3.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6dbe3c979c178231552ecba20214a8272df4e09f232a87aef4320cf06539aded"
|
||||
|
||||
[[package]]
|
||||
name = "blake2"
|
||||
version = "0.10.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe"
|
||||
dependencies = [
|
||||
"digest 0.10.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "blake3"
|
||||
version = "0.3.8"
|
||||
|
@ -1331,6 +1357,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
|||
dependencies = [
|
||||
"block-buffer 0.10.4",
|
||||
"crypto-common",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -2434,6 +2461,7 @@ dependencies = [
|
|||
name = "kanidm_lib_crypto"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"argon2",
|
||||
"base64 0.21.2",
|
||||
"base64urlsafedata",
|
||||
"hex",
|
||||
|
@ -3360,6 +3388,17 @@ dependencies = [
|
|||
"windows-targets 0.48.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 = "0.1.18"
|
||||
|
|
|
@ -39,6 +39,7 @@ homepage = "https://github.com/kanidm/kanidm/"
|
|||
repository = "https://github.com/kanidm/kanidm/"
|
||||
|
||||
[workspace.dependencies]
|
||||
argon2 = { version = "0.5.0", features = ["alloc"] }
|
||||
async-recursion = "1.0.4"
|
||||
async-trait = "^0.1.68"
|
||||
base32 = "^0.4.0"
|
||||
|
|
|
@ -4,6 +4,7 @@ version = "0.1.0"
|
|||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
argon2.workspace = true
|
||||
base64.workspace = true
|
||||
base64urlsafedata.workspace = true
|
||||
hex.workspace = true
|
||||
|
|
|
@ -1,6 +1,19 @@
|
|||
#![deny(warnings)]
|
||||
#![warn(unused_extern_crates)]
|
||||
#![deny(clippy::todo)]
|
||||
#![deny(clippy::unimplemented)]
|
||||
#![deny(clippy::unwrap_used)]
|
||||
#![deny(clippy::expect_used)]
|
||||
#![deny(clippy::panic)]
|
||||
#![deny(clippy::await_holding_lock)]
|
||||
#![deny(clippy::needless_pass_by_value)]
|
||||
#![deny(clippy::trivially_copy_pass_by_ref)]
|
||||
#![allow(clippy::unreachable)]
|
||||
|
||||
use argon2::{Algorithm, Argon2, Params, PasswordHash, Version};
|
||||
use base64::engine::GeneralPurpose;
|
||||
use base64::{alphabet, Engine};
|
||||
use tracing::{debug, error, warn};
|
||||
use tracing::{debug, error, info, trace, warn};
|
||||
|
||||
use base64::engine::general_purpose;
|
||||
use base64urlsafedata::Base64UrlSafeData;
|
||||
|
@ -16,7 +29,6 @@ use openssl::pkcs5::pbkdf2_hmac;
|
|||
use openssl::sha::Sha512;
|
||||
|
||||
// NIST 800-63.b salt should be 112 bits -> 14 8u8.
|
||||
// I choose tinfoil hat though ...
|
||||
const PBKDF2_SALT_LEN: usize = 24;
|
||||
|
||||
const PBKDF2_MIN_NIST_SALT_LEN: usize = 14;
|
||||
|
@ -32,9 +44,21 @@ const PBKDF2_SHA1_MIN_KEY_LEN: usize = 19;
|
|||
const DS_SSHA512_SALT_LEN: usize = 8;
|
||||
const DS_SSHA512_HASH_LEN: usize = 64;
|
||||
|
||||
const ARGON2_SALT_LEN: usize = 24;
|
||||
const ARGON2_KEY_LEN: usize = 32;
|
||||
const ARGON2_MAX_RAM_KIB: u32 = 32 * 1024;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[allow(non_camel_case_types)]
|
||||
pub enum DbPasswordV1 {
|
||||
ARGON2ID {
|
||||
m: u32,
|
||||
t: u32,
|
||||
p: u32,
|
||||
v: u32,
|
||||
s: Base64UrlSafeData,
|
||||
k: Base64UrlSafeData,
|
||||
},
|
||||
PBKDF2(usize, Vec<u8>, Vec<u8>),
|
||||
PBKDF2_SHA1(usize, Vec<u8>, Vec<u8>),
|
||||
PBKDF2_SHA512(usize, Vec<u8>, Vec<u8>),
|
||||
|
@ -45,6 +69,14 @@ pub enum DbPasswordV1 {
|
|||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
|
||||
#[allow(non_camel_case_types)]
|
||||
pub enum ReplPasswordV1 {
|
||||
ARGON2ID {
|
||||
m_cost: u32,
|
||||
t_cost: u32,
|
||||
p_cost: u32,
|
||||
version: u32,
|
||||
salt: Base64UrlSafeData,
|
||||
key: Base64UrlSafeData,
|
||||
},
|
||||
PBKDF2 {
|
||||
cost: usize,
|
||||
salt: Base64UrlSafeData,
|
||||
|
@ -72,6 +104,7 @@ pub enum ReplPasswordV1 {
|
|||
impl fmt::Debug for DbPasswordV1 {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
DbPasswordV1::ARGON2ID { .. } => write!(f, "ARGON2ID"),
|
||||
DbPasswordV1::PBKDF2(_, _, _) => write!(f, "PBKDF2"),
|
||||
DbPasswordV1::PBKDF2_SHA1(_, _, _) => write!(f, "PBKDF2_SHA1"),
|
||||
DbPasswordV1::PBKDF2_SHA512(_, _, _) => write!(f, "PBKDF2_SHA512"),
|
||||
|
@ -84,31 +117,37 @@ impl fmt::Debug for DbPasswordV1 {
|
|||
#[derive(Debug)]
|
||||
pub struct CryptoPolicy {
|
||||
pub(crate) pbkdf2_cost: usize,
|
||||
// https://docs.rs/argon2/0.5.0/argon2/struct.Params.html
|
||||
// defaults to 19mb memory, 2 iterations and 1 thread, with a 32byte output.
|
||||
pub(crate) argon2id_params: Params,
|
||||
}
|
||||
|
||||
impl CryptoPolicy {
|
||||
pub fn minimum() -> Self {
|
||||
CryptoPolicy {
|
||||
pbkdf2_cost: PBKDF2_MIN_NIST_COST,
|
||||
argon2id_params: Params::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn time_target(t: Duration) -> Self {
|
||||
let r = match Password::bench_pbkdf2(PBKDF2_MIN_NIST_COST * 10) {
|
||||
pub fn time_target(target_time: Duration) -> Self {
|
||||
const PBKDF2_BENCH_FACTOR: usize = 10;
|
||||
|
||||
let pbkdf2_cost = match Password::bench_pbkdf2(PBKDF2_MIN_NIST_COST * PBKDF2_BENCH_FACTOR) {
|
||||
Some(bt) => {
|
||||
let ubt = bt.as_nanos() as usize;
|
||||
|
||||
// Get the cost per thousand rounds
|
||||
let per_thou = (PBKDF2_MIN_NIST_COST * 10) / 1000;
|
||||
let per_thou = (PBKDF2_MIN_NIST_COST * PBKDF2_BENCH_FACTOR) / 1000;
|
||||
let t_per_thou = ubt / per_thou;
|
||||
// eprintln!("{} / {}", ubt, per_thou);
|
||||
trace!("{}µs / 1000 rounds", t_per_thou);
|
||||
|
||||
// Now we need the attacker work in nanos
|
||||
let attack_time = t.as_nanos() as usize;
|
||||
let r = (attack_time / t_per_thou) * 1000;
|
||||
let target = target_time.as_nanos() as usize;
|
||||
let r = (target / t_per_thou) * 1000;
|
||||
|
||||
// eprintln!("({} / {} ) * 1000", attack_time, t_per_thou);
|
||||
// eprintln!("Maybe rounds -> {}", r);
|
||||
trace!("{}µs target time", target);
|
||||
trace!("Maybe rounds -> {}", r);
|
||||
|
||||
if r < PBKDF2_MIN_NIST_COST {
|
||||
PBKDF2_MIN_NIST_COST
|
||||
|
@ -119,7 +158,78 @@ impl CryptoPolicy {
|
|||
None => PBKDF2_MIN_NIST_COST,
|
||||
};
|
||||
|
||||
CryptoPolicy { pbkdf2_cost: r }
|
||||
// Argon2id has multiple paramaters. These all are about *exchanges* that you can
|
||||
// request in how the computation is performed.
|
||||
//
|
||||
// rfc9106 explains that there are two algorithms stacked here. Argon2i has defences
|
||||
// against side-channel timing. Argon2d provides defences for time-memory tradeoffs.
|
||||
//
|
||||
// We can see how this impacts timings from sources like:
|
||||
// https://www.twelve21.io/how-to-choose-the-right-parameters-for-argon2/
|
||||
//
|
||||
// M = 256 MB, T = 2, d = 8, Time = 0.732 s
|
||||
// M = 128 MB, T = 6, d = 8, Time = 0.99 s
|
||||
// M = 64 MB, T = 12, d = 8, Time = 0.968 s
|
||||
// M = 32 MB, T = 24, d = 8, Time = 0.896 s
|
||||
// M = 16 MB, T = 49, d = 8, Time = 0.973 s
|
||||
// M = 8 MB, T = 96, d = 8, Time = 0.991 s
|
||||
// M = 4 MB, T = 190, d = 8, Time = 0.977 s
|
||||
// M = 2 MB, T = 271, d = 8, Time = 0.973 s
|
||||
// M = 1 MB, T = 639, d = 8, Time = 0.991 s
|
||||
//
|
||||
// As we can see, the time taken stays constant, but as ram decreases the amount of
|
||||
// CPU work required goes up. In our case, our primary threat is from GPU hashcat
|
||||
// cracking. GPU's tend to have many fast cores but very little amounts of fast ram
|
||||
// for those cores. So we want to have as much ram as *possible* up to a limit, and
|
||||
// then we want to increase iterations.
|
||||
//
|
||||
// This way a GPU has to expend further GPU time to compensate for the less ram.
|
||||
//
|
||||
// We also need to balance this against the fact we are a database, and we do have
|
||||
// caches. We also don't want to over-use RAM, especially because in the worst case
|
||||
// every thread will be operationg in argon2id at the same time. That means
|
||||
// thread x ram will be used. If we had 8 threads at 64mb of ram, that would require
|
||||
// 512mb of ram alone just for hashing. This becomes worse as core counts scale, with
|
||||
// 24 core xeons easily reaching 1.5GB in these cases.
|
||||
//
|
||||
// To try to balance this we cap max ram at 32MB for now.
|
||||
|
||||
let mut t = Duration::ZERO;
|
||||
// Default amount of ram we sacrifice per thread is 8MB
|
||||
let mut m_cost = 8 * 1024;
|
||||
// Default t/p from argon2 library.
|
||||
let t_cost = 2;
|
||||
let p_cost = 1;
|
||||
|
||||
// Raise memory usage until an acceptable ram amount is reached.
|
||||
while t < target_time && m_cost < ARGON2_MAX_RAM_KIB {
|
||||
m_cost += 1024;
|
||||
let params = if let Ok(p) = Params::new(m_cost, t_cost, p_cost, None) {
|
||||
p
|
||||
} else {
|
||||
// Unable to proceed.
|
||||
break;
|
||||
};
|
||||
|
||||
if let Some(ubt) = Password::bench_argon2id(params) {
|
||||
t = ubt;
|
||||
trace!("{}µs for m_cost {}", t.as_nanos(), m_cost);
|
||||
} else {
|
||||
error!("Unable to perform bench of argon2id, stopping benchmark");
|
||||
t = Duration::MAX;
|
||||
}
|
||||
}
|
||||
|
||||
let argon2id_params = Params::new(m_cost, t_cost, p_cost, None)
|
||||
// fallback
|
||||
.unwrap_or_default();
|
||||
|
||||
let p = CryptoPolicy {
|
||||
pbkdf2_cost,
|
||||
argon2id_params,
|
||||
};
|
||||
info!(pbkdf2_cost = %p.pbkdf2_cost, argon2id_m = %p.argon2id_params.m_cost(), argon2id_p = %p.argon2id_params.p_cost(), argon2id_t = %p.argon2id_params.t_cost(), );
|
||||
p
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -129,6 +239,15 @@ impl CryptoPolicy {
|
|||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[allow(non_camel_case_types)]
|
||||
enum Kdf {
|
||||
//
|
||||
ARGON2ID {
|
||||
m_cost: u32,
|
||||
t_cost: u32,
|
||||
p_cost: u32,
|
||||
version: u32,
|
||||
salt: Vec<u8>,
|
||||
key: Vec<u8>,
|
||||
},
|
||||
// cost, salt, hash
|
||||
PBKDF2(usize, Vec<u8>, Vec<u8>),
|
||||
|
||||
|
@ -153,6 +272,16 @@ impl TryFrom<DbPasswordV1> for Password {
|
|||
|
||||
fn try_from(value: DbPasswordV1) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
DbPasswordV1::ARGON2ID { m, t, p, v, s, k } => Ok(Password {
|
||||
material: Kdf::ARGON2ID {
|
||||
m_cost: m,
|
||||
t_cost: t,
|
||||
p_cost: p,
|
||||
version: v,
|
||||
salt: s.into(),
|
||||
key: k.into(),
|
||||
},
|
||||
}),
|
||||
DbPasswordV1::PBKDF2(c, s, h) => Ok(Password {
|
||||
material: Kdf::PBKDF2(c, s, h),
|
||||
}),
|
||||
|
@ -177,6 +306,23 @@ impl TryFrom<&ReplPasswordV1> for Password {
|
|||
|
||||
fn try_from(value: &ReplPasswordV1) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
ReplPasswordV1::ARGON2ID {
|
||||
m_cost,
|
||||
t_cost,
|
||||
p_cost,
|
||||
version,
|
||||
salt,
|
||||
key,
|
||||
} => Ok(Password {
|
||||
material: Kdf::ARGON2ID {
|
||||
m_cost: *m_cost,
|
||||
t_cost: *t_cost,
|
||||
p_cost: *p_cost,
|
||||
version: *version,
|
||||
salt: salt.0.clone(),
|
||||
key: key.0.clone(),
|
||||
},
|
||||
}),
|
||||
ReplPasswordV1::PBKDF2 { cost, salt, hash } => Ok(Password {
|
||||
material: Kdf::PBKDF2(*cost, salt.0.clone(), hash.0.clone()),
|
||||
}),
|
||||
|
@ -367,6 +513,72 @@ impl TryFrom<&str> for Password {
|
|||
}
|
||||
}
|
||||
|
||||
if let Some(argon2_phc) = value.strip_prefix("{ARGON2}") {
|
||||
match PasswordHash::try_from(argon2_phc) {
|
||||
Ok(PasswordHash {
|
||||
algorithm,
|
||||
version,
|
||||
params,
|
||||
salt,
|
||||
hash,
|
||||
}) => {
|
||||
if algorithm.as_str() != "argon2id" {
|
||||
error!(alg = %algorithm.as_str(), "Only argon2id is supported");
|
||||
return Err(());
|
||||
}
|
||||
|
||||
let version = version.unwrap_or(19);
|
||||
let version: Version = version.try_into().map_err(|_| {
|
||||
error!("Failed to convert {} to valid argon2id version", version);
|
||||
})?;
|
||||
|
||||
let m_cost = params.get_decimal("m").ok_or_else(|| {
|
||||
error!("Failed to access m_cost parameter");
|
||||
})?;
|
||||
|
||||
let t_cost = params.get_decimal("t").ok_or_else(|| {
|
||||
error!("Failed to access t_cost parameter");
|
||||
})?;
|
||||
|
||||
let p_cost = params.get_decimal("p").ok_or_else(|| {
|
||||
error!("Failed to access p_cost parameter");
|
||||
})?;
|
||||
|
||||
let salt = salt
|
||||
.and_then(|s| {
|
||||
let mut salt_arr = [0u8; 64];
|
||||
s.decode_b64(&mut salt_arr)
|
||||
.ok()
|
||||
.map(|salt_bytes| salt_bytes.to_owned())
|
||||
})
|
||||
.ok_or_else(|| {
|
||||
error!("Failed to access salt");
|
||||
})?;
|
||||
|
||||
error!(?salt);
|
||||
|
||||
let key = hash.map(|h| h.as_bytes().into()).ok_or_else(|| {
|
||||
error!("Failed to access key");
|
||||
})?;
|
||||
|
||||
return Ok(Password {
|
||||
material: Kdf::ARGON2ID {
|
||||
m_cost,
|
||||
t_cost,
|
||||
p_cost,
|
||||
version: version as u32,
|
||||
salt,
|
||||
key,
|
||||
},
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
error!(?e, "Invalid argon2 phc string");
|
||||
return Err(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Nothing matched to this point.
|
||||
Err(())
|
||||
}
|
||||
|
@ -394,7 +606,26 @@ impl Password {
|
|||
end.checked_duration_since(start)
|
||||
}
|
||||
|
||||
fn new_pbkdf2(pbkdf2_cost: usize, cleartext: &str) -> Result<Kdf, OperationError> {
|
||||
fn bench_argon2id(params: Params) -> Option<Duration> {
|
||||
let mut rng = rand::thread_rng();
|
||||
let salt: Vec<u8> = (0..PBKDF2_SALT_LEN).map(|_| rng.gen()).collect();
|
||||
let input: Vec<u8> = (0..PBKDF2_SALT_LEN).map(|_| rng.gen()).collect();
|
||||
// This is 512 bits of output
|
||||
let mut key: Vec<u8> = (0..PBKDF2_KEY_LEN).map(|_| 0).collect();
|
||||
|
||||
let argon = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
|
||||
|
||||
let start = Instant::now();
|
||||
argon
|
||||
.hash_password_into(input.as_slice(), salt.as_slice(), key.as_mut_slice())
|
||||
.ok()?;
|
||||
let end = Instant::now();
|
||||
|
||||
end.checked_duration_since(start)
|
||||
}
|
||||
|
||||
pub fn new_pbkdf2(policy: &CryptoPolicy, cleartext: &str) -> Result<Self, OperationError> {
|
||||
let pbkdf2_cost = policy.pbkdf2_cost;
|
||||
let mut rng = rand::thread_rng();
|
||||
let salt: Vec<u8> = (0..PBKDF2_SALT_LEN).map(|_| rng.gen()).collect();
|
||||
// This is 512 bits of output
|
||||
|
@ -412,14 +643,78 @@ impl Password {
|
|||
Kdf::PBKDF2(pbkdf2_cost, salt, key)
|
||||
})
|
||||
.map_err(|_| OperationError::CryptographyError)
|
||||
.map(|material| Password { material })
|
||||
}
|
||||
|
||||
pub fn new_argon2id(policy: &CryptoPolicy, cleartext: &str) -> Result<Self, OperationError> {
|
||||
let version = Version::V0x13;
|
||||
|
||||
let argon = Argon2::new(Algorithm::Argon2id, version, policy.argon2id_params.clone());
|
||||
|
||||
let mut rng = rand::thread_rng();
|
||||
let salt: Vec<u8> = (0..ARGON2_SALT_LEN).map(|_| rng.gen()).collect();
|
||||
let mut key: Vec<u8> = (0..ARGON2_KEY_LEN).map(|_| 0).collect();
|
||||
|
||||
argon
|
||||
.hash_password_into(cleartext.as_bytes(), salt.as_slice(), key.as_mut_slice())
|
||||
.map(|()| Kdf::ARGON2ID {
|
||||
m_cost: policy.argon2id_params.m_cost(),
|
||||
t_cost: policy.argon2id_params.t_cost(),
|
||||
p_cost: policy.argon2id_params.p_cost(),
|
||||
version: version as u32,
|
||||
salt,
|
||||
key,
|
||||
})
|
||||
.map_err(|_| OperationError::CryptographyError)
|
||||
.map(|material| Password { material })
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn new(policy: &CryptoPolicy, cleartext: &str) -> Result<Self, OperationError> {
|
||||
Self::new_pbkdf2(policy.pbkdf2_cost, cleartext).map(|material| Password { material })
|
||||
Self::new_pbkdf2(policy, cleartext)
|
||||
}
|
||||
|
||||
pub fn verify(&self, cleartext: &str) -> Result<bool, OperationError> {
|
||||
match &self.material {
|
||||
Kdf::ARGON2ID {
|
||||
m_cost,
|
||||
t_cost,
|
||||
p_cost,
|
||||
version,
|
||||
salt,
|
||||
key,
|
||||
} => {
|
||||
let version: Version = (*version).try_into().map_err(|_| {
|
||||
error!("Failed to convert {} to valid argon2id version", version);
|
||||
OperationError::CryptographyError
|
||||
})?;
|
||||
|
||||
let key_len = key.len();
|
||||
|
||||
let params =
|
||||
Params::new(*m_cost, *t_cost, *p_cost, Some(key_len)).map_err(|e| {
|
||||
error!(err = ?e, "invalid argon2id parameters");
|
||||
OperationError::CryptographyError
|
||||
})?;
|
||||
|
||||
let argon = Argon2::new(Algorithm::Argon2id, version, params);
|
||||
let mut check_key: Vec<u8> = (0..key_len).map(|_| 0).collect();
|
||||
|
||||
argon
|
||||
.hash_password_into(
|
||||
cleartext.as_bytes(),
|
||||
salt.as_slice(),
|
||||
check_key.as_mut_slice(),
|
||||
)
|
||||
.map_err(|e| {
|
||||
error!(err = ?e, "unable to perform argon2id hash");
|
||||
OperationError::CryptographyError
|
||||
})
|
||||
.map(|()| {
|
||||
// Actually compare the outputs.
|
||||
&check_key == key
|
||||
})
|
||||
}
|
||||
Kdf::PBKDF2(cost, salt, key) => {
|
||||
// We have to get the number of bits to derive from our stored hash
|
||||
// as some imported hash types may have variable lengths
|
||||
|
@ -508,6 +803,21 @@ impl Password {
|
|||
|
||||
pub fn to_dbpasswordv1(&self) -> DbPasswordV1 {
|
||||
match &self.material {
|
||||
Kdf::ARGON2ID {
|
||||
m_cost,
|
||||
t_cost,
|
||||
p_cost,
|
||||
version,
|
||||
salt,
|
||||
key,
|
||||
} => DbPasswordV1::ARGON2ID {
|
||||
m: *m_cost,
|
||||
t: *t_cost,
|
||||
p: *p_cost,
|
||||
v: *version,
|
||||
s: salt.clone().into(),
|
||||
k: key.clone().into(),
|
||||
},
|
||||
Kdf::PBKDF2(cost, salt, hash) => {
|
||||
DbPasswordV1::PBKDF2(*cost, salt.clone(), hash.clone())
|
||||
}
|
||||
|
@ -524,6 +834,21 @@ impl Password {
|
|||
|
||||
pub fn to_repl_v1(&self) -> ReplPasswordV1 {
|
||||
match &self.material {
|
||||
Kdf::ARGON2ID {
|
||||
m_cost,
|
||||
t_cost,
|
||||
p_cost,
|
||||
version,
|
||||
salt,
|
||||
key,
|
||||
} => ReplPasswordV1::ARGON2ID {
|
||||
m_cost: *m_cost,
|
||||
t_cost: *t_cost,
|
||||
p_cost: *p_cost,
|
||||
version: *version,
|
||||
salt: salt.clone().into(),
|
||||
key: key.clone().into(),
|
||||
},
|
||||
Kdf::PBKDF2(cost, salt, hash) => ReplPasswordV1::PBKDF2 {
|
||||
cost: *cost,
|
||||
salt: salt.clone().into(),
|
||||
|
@ -551,6 +876,7 @@ impl Password {
|
|||
|
||||
pub fn requires_upgrade(&self) -> bool {
|
||||
match &self.material {
|
||||
Kdf::ARGON2ID { .. } => false,
|
||||
Kdf::PBKDF2_SHA512(cost, salt, hash) | Kdf::PBKDF2(cost, salt, hash) => {
|
||||
*cost < PBKDF2_MIN_NIST_COST
|
||||
|| salt.len() < PBKDF2_MIN_NIST_SALT_LEN
|
||||
|
@ -578,6 +904,24 @@ mod tests {
|
|||
assert!(!c.verify("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_password_pbkdf2() {
|
||||
let p = CryptoPolicy::minimum();
|
||||
let c = Password::new_pbkdf2(&p, "password").unwrap();
|
||||
assert!(c.verify("password").unwrap());
|
||||
assert!(!c.verify("password1").unwrap());
|
||||
assert!(!c.verify("Password1").unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_password_argon2id() {
|
||||
let p = CryptoPolicy::minimum();
|
||||
let c = Password::new_argon2id(&p, "password").unwrap();
|
||||
assert!(c.verify("password").unwrap());
|
||||
assert!(!c.verify("password1").unwrap());
|
||||
assert!(!c.verify("Password1").unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_password_from_invalid() {
|
||||
assert!(Password::try_from("password").is_err())
|
||||
|
@ -640,17 +984,16 @@ mod tests {
|
|||
assert!(r.verify(password).unwrap_or(false));
|
||||
}
|
||||
|
||||
/*
|
||||
// Not supported in openssl, may need an external crate.
|
||||
#[test]
|
||||
fn test_password_from_openldap_argon2() {
|
||||
let im_pw = "{ARGON2}$argon2id$v=19$m=65536,t=2,p=1$IyTQMsvzB2JHDiWx8fq7Ew$VhYOA7AL0kbRXI5g2kOyyp8St1epkNj7WZyUY4pAIQQ"
|
||||
sketching::test_init();
|
||||
let im_pw = "{ARGON2}$argon2id$v=19$m=65536,t=2,p=1$IyTQMsvzB2JHDiWx8fq7Ew$VhYOA7AL0kbRXI5g2kOyyp8St1epkNj7WZyUY4pAIQQ";
|
||||
let password = "password";
|
||||
let r = Password::try_from(im_pw).expect("Failed to parse");
|
||||
assert!(r.requires_upgrade());
|
||||
assert!(!r.requires_upgrade());
|
||||
assert!(r.verify(password).unwrap_or(false));
|
||||
}
|
||||
*/
|
||||
|
||||
/*
|
||||
* wbrown - 20221104 - I tried to programmatically enable the legacy provider, but
|
||||
|
|
|
@ -153,13 +153,8 @@ impl IdmServer {
|
|||
origin: &str,
|
||||
) -> Result<(IdmServer, IdmServerDelayed, IdmServerAudit), OperationError> {
|
||||
// This is calculated back from:
|
||||
// 500 auths / thread -> 0.002 sec per op
|
||||
// we can then spend up to ~0.001s hashing
|
||||
// that means an attacker could possibly have
|
||||
// 1000 attempts/sec on a compromised pw.
|
||||
// overtime, we could increase this as auth parallelism
|
||||
// improves.
|
||||
let crypto_policy = CryptoPolicy::time_target(Duration::from_millis(1));
|
||||
// 100 password auths / thread -> 0.010 sec per op
|
||||
let crypto_policy = CryptoPolicy::time_target(Duration::from_millis(10));
|
||||
let (async_tx, async_rx) = unbounded();
|
||||
let (audit_tx, audit_rx) = unbounded();
|
||||
|
||||
|
|
|
@ -105,10 +105,7 @@ impl CacheLayer {
|
|||
home_alias,
|
||||
uid_attr_map,
|
||||
gid_attr_map,
|
||||
allow_id_overrides: allow_id_overrides
|
||||
.into_iter()
|
||||
.map(|name| Id::Name(name))
|
||||
.collect(),
|
||||
allow_id_overrides: allow_id_overrides.into_iter().map(Id::Name).collect(),
|
||||
nxset: Mutex::new(HashSet::new()),
|
||||
nxcache: Mutex::new(LruCache::new(NXCACHE_SIZE)),
|
||||
})
|
||||
|
|
Loading…
Reference in a new issue