mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 20:47:01 +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"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d301b3b94cb4b2f23d7917810addbbaff90738e0ca2be692bd027e70d7e0330c"
|
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]]
|
[[package]]
|
||||||
name = "arrayref"
|
name = "arrayref"
|
||||||
version = "0.3.7"
|
version = "0.3.7"
|
||||||
|
@ -506,6 +517,12 @@ version = "0.21.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d"
|
checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base64ct"
|
||||||
|
version = "1.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "base64urlsafedata"
|
name = "base64urlsafedata"
|
||||||
version = "0.1.3"
|
version = "0.1.3"
|
||||||
|
@ -576,6 +593,15 @@ version = "2.3.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6dbe3c979c178231552ecba20214a8272df4e09f232a87aef4320cf06539aded"
|
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]]
|
[[package]]
|
||||||
name = "blake3"
|
name = "blake3"
|
||||||
version = "0.3.8"
|
version = "0.3.8"
|
||||||
|
@ -1331,6 +1357,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"block-buffer 0.10.4",
|
"block-buffer 0.10.4",
|
||||||
"crypto-common",
|
"crypto-common",
|
||||||
|
"subtle",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -2434,6 +2461,7 @@ dependencies = [
|
||||||
name = "kanidm_lib_crypto"
|
name = "kanidm_lib_crypto"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"argon2",
|
||||||
"base64 0.21.2",
|
"base64 0.21.2",
|
||||||
"base64urlsafedata",
|
"base64urlsafedata",
|
||||||
"hex",
|
"hex",
|
||||||
|
@ -3360,6 +3388,17 @@ dependencies = [
|
||||||
"windows-targets 0.48.0",
|
"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]]
|
[[package]]
|
||||||
name = "paste"
|
name = "paste"
|
||||||
version = "0.1.18"
|
version = "0.1.18"
|
||||||
|
|
|
@ -39,6 +39,7 @@ homepage = "https://github.com/kanidm/kanidm/"
|
||||||
repository = "https://github.com/kanidm/kanidm/"
|
repository = "https://github.com/kanidm/kanidm/"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
|
argon2 = { version = "0.5.0", features = ["alloc"] }
|
||||||
async-recursion = "1.0.4"
|
async-recursion = "1.0.4"
|
||||||
async-trait = "^0.1.68"
|
async-trait = "^0.1.68"
|
||||||
base32 = "^0.4.0"
|
base32 = "^0.4.0"
|
||||||
|
|
|
@ -4,6 +4,7 @@ version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
argon2.workspace = true
|
||||||
base64.workspace = true
|
base64.workspace = true
|
||||||
base64urlsafedata.workspace = true
|
base64urlsafedata.workspace = true
|
||||||
hex.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::engine::GeneralPurpose;
|
||||||
use base64::{alphabet, Engine};
|
use base64::{alphabet, Engine};
|
||||||
use tracing::{debug, error, warn};
|
use tracing::{debug, error, info, trace, warn};
|
||||||
|
|
||||||
use base64::engine::general_purpose;
|
use base64::engine::general_purpose;
|
||||||
use base64urlsafedata::Base64UrlSafeData;
|
use base64urlsafedata::Base64UrlSafeData;
|
||||||
|
@ -16,7 +29,6 @@ use openssl::pkcs5::pbkdf2_hmac;
|
||||||
use openssl::sha::Sha512;
|
use openssl::sha::Sha512;
|
||||||
|
|
||||||
// NIST 800-63.b salt should be 112 bits -> 14 8u8.
|
// NIST 800-63.b salt should be 112 bits -> 14 8u8.
|
||||||
// I choose tinfoil hat though ...
|
|
||||||
const PBKDF2_SALT_LEN: usize = 24;
|
const PBKDF2_SALT_LEN: usize = 24;
|
||||||
|
|
||||||
const PBKDF2_MIN_NIST_SALT_LEN: usize = 14;
|
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_SALT_LEN: usize = 8;
|
||||||
const DS_SSHA512_HASH_LEN: usize = 64;
|
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)]
|
#[derive(Serialize, Deserialize)]
|
||||||
#[allow(non_camel_case_types)]
|
#[allow(non_camel_case_types)]
|
||||||
pub enum DbPasswordV1 {
|
pub enum DbPasswordV1 {
|
||||||
|
ARGON2ID {
|
||||||
|
m: u32,
|
||||||
|
t: u32,
|
||||||
|
p: u32,
|
||||||
|
v: u32,
|
||||||
|
s: Base64UrlSafeData,
|
||||||
|
k: Base64UrlSafeData,
|
||||||
|
},
|
||||||
PBKDF2(usize, Vec<u8>, Vec<u8>),
|
PBKDF2(usize, Vec<u8>, Vec<u8>),
|
||||||
PBKDF2_SHA1(usize, Vec<u8>, Vec<u8>),
|
PBKDF2_SHA1(usize, Vec<u8>, Vec<u8>),
|
||||||
PBKDF2_SHA512(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)]
|
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
|
||||||
#[allow(non_camel_case_types)]
|
#[allow(non_camel_case_types)]
|
||||||
pub enum ReplPasswordV1 {
|
pub enum ReplPasswordV1 {
|
||||||
|
ARGON2ID {
|
||||||
|
m_cost: u32,
|
||||||
|
t_cost: u32,
|
||||||
|
p_cost: u32,
|
||||||
|
version: u32,
|
||||||
|
salt: Base64UrlSafeData,
|
||||||
|
key: Base64UrlSafeData,
|
||||||
|
},
|
||||||
PBKDF2 {
|
PBKDF2 {
|
||||||
cost: usize,
|
cost: usize,
|
||||||
salt: Base64UrlSafeData,
|
salt: Base64UrlSafeData,
|
||||||
|
@ -72,6 +104,7 @@ pub enum ReplPasswordV1 {
|
||||||
impl fmt::Debug for DbPasswordV1 {
|
impl fmt::Debug for DbPasswordV1 {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
|
DbPasswordV1::ARGON2ID { .. } => write!(f, "ARGON2ID"),
|
||||||
DbPasswordV1::PBKDF2(_, _, _) => write!(f, "PBKDF2"),
|
DbPasswordV1::PBKDF2(_, _, _) => write!(f, "PBKDF2"),
|
||||||
DbPasswordV1::PBKDF2_SHA1(_, _, _) => write!(f, "PBKDF2_SHA1"),
|
DbPasswordV1::PBKDF2_SHA1(_, _, _) => write!(f, "PBKDF2_SHA1"),
|
||||||
DbPasswordV1::PBKDF2_SHA512(_, _, _) => write!(f, "PBKDF2_SHA512"),
|
DbPasswordV1::PBKDF2_SHA512(_, _, _) => write!(f, "PBKDF2_SHA512"),
|
||||||
|
@ -84,31 +117,37 @@ impl fmt::Debug for DbPasswordV1 {
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct CryptoPolicy {
|
pub struct CryptoPolicy {
|
||||||
pub(crate) pbkdf2_cost: usize,
|
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 {
|
impl CryptoPolicy {
|
||||||
pub fn minimum() -> Self {
|
pub fn minimum() -> Self {
|
||||||
CryptoPolicy {
|
CryptoPolicy {
|
||||||
pbkdf2_cost: PBKDF2_MIN_NIST_COST,
|
pbkdf2_cost: PBKDF2_MIN_NIST_COST,
|
||||||
|
argon2id_params: Params::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn time_target(t: Duration) -> Self {
|
pub fn time_target(target_time: Duration) -> Self {
|
||||||
let r = match Password::bench_pbkdf2(PBKDF2_MIN_NIST_COST * 10) {
|
const PBKDF2_BENCH_FACTOR: usize = 10;
|
||||||
|
|
||||||
|
let pbkdf2_cost = match Password::bench_pbkdf2(PBKDF2_MIN_NIST_COST * PBKDF2_BENCH_FACTOR) {
|
||||||
Some(bt) => {
|
Some(bt) => {
|
||||||
let ubt = bt.as_nanos() as usize;
|
let ubt = bt.as_nanos() as usize;
|
||||||
|
|
||||||
// Get the cost per thousand rounds
|
// 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;
|
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
|
// Now we need the attacker work in nanos
|
||||||
let attack_time = t.as_nanos() as usize;
|
let target = target_time.as_nanos() as usize;
|
||||||
let r = (attack_time / t_per_thou) * 1000;
|
let r = (target / t_per_thou) * 1000;
|
||||||
|
|
||||||
// eprintln!("({} / {} ) * 1000", attack_time, t_per_thou);
|
trace!("{}µs target time", target);
|
||||||
// eprintln!("Maybe rounds -> {}", r);
|
trace!("Maybe rounds -> {}", r);
|
||||||
|
|
||||||
if r < PBKDF2_MIN_NIST_COST {
|
if r < PBKDF2_MIN_NIST_COST {
|
||||||
PBKDF2_MIN_NIST_COST
|
PBKDF2_MIN_NIST_COST
|
||||||
|
@ -119,7 +158,78 @@ impl CryptoPolicy {
|
||||||
None => PBKDF2_MIN_NIST_COST,
|
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)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
#[allow(non_camel_case_types)]
|
#[allow(non_camel_case_types)]
|
||||||
enum Kdf {
|
enum Kdf {
|
||||||
|
//
|
||||||
|
ARGON2ID {
|
||||||
|
m_cost: u32,
|
||||||
|
t_cost: u32,
|
||||||
|
p_cost: u32,
|
||||||
|
version: u32,
|
||||||
|
salt: Vec<u8>,
|
||||||
|
key: Vec<u8>,
|
||||||
|
},
|
||||||
// cost, salt, hash
|
// cost, salt, hash
|
||||||
PBKDF2(usize, Vec<u8>, Vec<u8>),
|
PBKDF2(usize, Vec<u8>, Vec<u8>),
|
||||||
|
|
||||||
|
@ -153,6 +272,16 @@ impl TryFrom<DbPasswordV1> for Password {
|
||||||
|
|
||||||
fn try_from(value: DbPasswordV1) -> Result<Self, Self::Error> {
|
fn try_from(value: DbPasswordV1) -> Result<Self, Self::Error> {
|
||||||
match value {
|
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 {
|
DbPasswordV1::PBKDF2(c, s, h) => Ok(Password {
|
||||||
material: Kdf::PBKDF2(c, s, h),
|
material: Kdf::PBKDF2(c, s, h),
|
||||||
}),
|
}),
|
||||||
|
@ -177,6 +306,23 @@ impl TryFrom<&ReplPasswordV1> for Password {
|
||||||
|
|
||||||
fn try_from(value: &ReplPasswordV1) -> Result<Self, Self::Error> {
|
fn try_from(value: &ReplPasswordV1) -> Result<Self, Self::Error> {
|
||||||
match value {
|
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 {
|
ReplPasswordV1::PBKDF2 { cost, salt, hash } => Ok(Password {
|
||||||
material: Kdf::PBKDF2(*cost, salt.0.clone(), hash.0.clone()),
|
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.
|
// Nothing matched to this point.
|
||||||
Err(())
|
Err(())
|
||||||
}
|
}
|
||||||
|
@ -394,7 +606,26 @@ impl Password {
|
||||||
end.checked_duration_since(start)
|
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 mut rng = rand::thread_rng();
|
||||||
let salt: Vec<u8> = (0..PBKDF2_SALT_LEN).map(|_| rng.gen()).collect();
|
let salt: Vec<u8> = (0..PBKDF2_SALT_LEN).map(|_| rng.gen()).collect();
|
||||||
// This is 512 bits of output
|
// This is 512 bits of output
|
||||||
|
@ -412,14 +643,78 @@ impl Password {
|
||||||
Kdf::PBKDF2(pbkdf2_cost, salt, key)
|
Kdf::PBKDF2(pbkdf2_cost, salt, key)
|
||||||
})
|
})
|
||||||
.map_err(|_| OperationError::CryptographyError)
|
.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> {
|
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> {
|
pub fn verify(&self, cleartext: &str) -> Result<bool, OperationError> {
|
||||||
match &self.material {
|
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) => {
|
Kdf::PBKDF2(cost, salt, key) => {
|
||||||
// We have to get the number of bits to derive from our stored hash
|
// We have to get the number of bits to derive from our stored hash
|
||||||
// as some imported hash types may have variable lengths
|
// as some imported hash types may have variable lengths
|
||||||
|
@ -508,6 +803,21 @@ impl Password {
|
||||||
|
|
||||||
pub fn to_dbpasswordv1(&self) -> DbPasswordV1 {
|
pub fn to_dbpasswordv1(&self) -> DbPasswordV1 {
|
||||||
match &self.material {
|
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) => {
|
Kdf::PBKDF2(cost, salt, hash) => {
|
||||||
DbPasswordV1::PBKDF2(*cost, salt.clone(), hash.clone())
|
DbPasswordV1::PBKDF2(*cost, salt.clone(), hash.clone())
|
||||||
}
|
}
|
||||||
|
@ -524,6 +834,21 @@ impl Password {
|
||||||
|
|
||||||
pub fn to_repl_v1(&self) -> ReplPasswordV1 {
|
pub fn to_repl_v1(&self) -> ReplPasswordV1 {
|
||||||
match &self.material {
|
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 {
|
Kdf::PBKDF2(cost, salt, hash) => ReplPasswordV1::PBKDF2 {
|
||||||
cost: *cost,
|
cost: *cost,
|
||||||
salt: salt.clone().into(),
|
salt: salt.clone().into(),
|
||||||
|
@ -551,6 +876,7 @@ impl Password {
|
||||||
|
|
||||||
pub fn requires_upgrade(&self) -> bool {
|
pub fn requires_upgrade(&self) -> bool {
|
||||||
match &self.material {
|
match &self.material {
|
||||||
|
Kdf::ARGON2ID { .. } => false,
|
||||||
Kdf::PBKDF2_SHA512(cost, salt, hash) | Kdf::PBKDF2(cost, salt, hash) => {
|
Kdf::PBKDF2_SHA512(cost, salt, hash) | Kdf::PBKDF2(cost, salt, hash) => {
|
||||||
*cost < PBKDF2_MIN_NIST_COST
|
*cost < PBKDF2_MIN_NIST_COST
|
||||||
|| salt.len() < PBKDF2_MIN_NIST_SALT_LEN
|
|| salt.len() < PBKDF2_MIN_NIST_SALT_LEN
|
||||||
|
@ -578,6 +904,24 @@ mod tests {
|
||||||
assert!(!c.verify("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").unwrap());
|
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]
|
#[test]
|
||||||
fn test_password_from_invalid() {
|
fn test_password_from_invalid() {
|
||||||
assert!(Password::try_from("password").is_err())
|
assert!(Password::try_from("password").is_err())
|
||||||
|
@ -640,17 +984,16 @@ mod tests {
|
||||||
assert!(r.verify(password).unwrap_or(false));
|
assert!(r.verify(password).unwrap_or(false));
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
// Not supported in openssl, may need an external crate.
|
// Not supported in openssl, may need an external crate.
|
||||||
#[test]
|
#[test]
|
||||||
fn test_password_from_openldap_argon2() {
|
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 password = "password";
|
||||||
let r = Password::try_from(im_pw).expect("Failed to parse");
|
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));
|
assert!(r.verify(password).unwrap_or(false));
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* wbrown - 20221104 - I tried to programmatically enable the legacy provider, but
|
* wbrown - 20221104 - I tried to programmatically enable the legacy provider, but
|
||||||
|
|
|
@ -153,13 +153,8 @@ impl IdmServer {
|
||||||
origin: &str,
|
origin: &str,
|
||||||
) -> Result<(IdmServer, IdmServerDelayed, IdmServerAudit), OperationError> {
|
) -> Result<(IdmServer, IdmServerDelayed, IdmServerAudit), OperationError> {
|
||||||
// This is calculated back from:
|
// This is calculated back from:
|
||||||
// 500 auths / thread -> 0.002 sec per op
|
// 100 password auths / thread -> 0.010 sec per op
|
||||||
// we can then spend up to ~0.001s hashing
|
let crypto_policy = CryptoPolicy::time_target(Duration::from_millis(10));
|
||||||
// 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));
|
|
||||||
let (async_tx, async_rx) = unbounded();
|
let (async_tx, async_rx) = unbounded();
|
||||||
let (audit_tx, audit_rx) = unbounded();
|
let (audit_tx, audit_rx) = unbounded();
|
||||||
|
|
||||||
|
|
|
@ -105,10 +105,7 @@ impl CacheLayer {
|
||||||
home_alias,
|
home_alias,
|
||||||
uid_attr_map,
|
uid_attr_map,
|
||||||
gid_attr_map,
|
gid_attr_map,
|
||||||
allow_id_overrides: allow_id_overrides
|
allow_id_overrides: allow_id_overrides.into_iter().map(Id::Name).collect(),
|
||||||
.into_iter()
|
|
||||||
.map(|name| Id::Name(name))
|
|
||||||
.collect(),
|
|
||||||
nxset: Mutex::new(HashSet::new()),
|
nxset: Mutex::new(HashSet::new()),
|
||||||
nxcache: Mutex::new(LruCache::new(NXCACHE_SIZE)),
|
nxcache: Mutex::new(LruCache::new(NXCACHE_SIZE)),
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in a new issue