mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 20:47:01 +01:00
Dynamic crypto rounds (#311)
This commit is contained in:
parent
e34a848a88
commit
bd8d2af420
|
@ -5,22 +5,26 @@ use r2d2_sqlite::SqliteConnectionManager;
|
|||
use rusqlite::NO_PARAMS;
|
||||
use std::convert::TryFrom;
|
||||
use std::fmt;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::cache::Id;
|
||||
use tokio::sync::{Mutex, MutexGuard};
|
||||
|
||||
use kanidm::be::dbvalue::DbPasswordV1;
|
||||
use kanidm::credential::policy::CryptoPolicy;
|
||||
use kanidm::credential::Password;
|
||||
|
||||
pub struct Db {
|
||||
pool: Pool<SqliteConnectionManager>,
|
||||
lock: Mutex<()>,
|
||||
crypto_policy: CryptoPolicy,
|
||||
}
|
||||
|
||||
pub struct DbTxn<'a> {
|
||||
_guard: MutexGuard<'a, ()>,
|
||||
committed: bool,
|
||||
conn: r2d2::PooledConnection<SqliteConnectionManager>,
|
||||
crypto_policy: &'a CryptoPolicy,
|
||||
}
|
||||
|
||||
impl Db {
|
||||
|
@ -35,9 +39,14 @@ impl Db {
|
|||
error!("r2d2 error {:?}", e);
|
||||
})?;
|
||||
|
||||
let crypto_policy = CryptoPolicy::time_target(Duration::from_millis(250));
|
||||
|
||||
debug!("Configured {:?}", crypto_policy);
|
||||
|
||||
Ok(Db {
|
||||
pool,
|
||||
lock: Mutex::new(()),
|
||||
crypto_policy,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -48,7 +57,7 @@ impl Db {
|
|||
.pool
|
||||
.get()
|
||||
.expect("Unable to get connection from pool!!!");
|
||||
DbTxn::new(conn, guard)
|
||||
DbTxn::new(conn, guard, &self.crypto_policy)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -62,6 +71,7 @@ impl<'a> DbTxn<'a> {
|
|||
pub fn new(
|
||||
conn: r2d2::PooledConnection<SqliteConnectionManager>,
|
||||
guard: MutexGuard<'a, ()>,
|
||||
crypto_policy: &'a CryptoPolicy,
|
||||
) -> Self {
|
||||
// Start the transaction
|
||||
// debug!("Starting db WR txn ...");
|
||||
|
@ -72,6 +82,7 @@ impl<'a> DbTxn<'a> {
|
|||
committed: false,
|
||||
conn,
|
||||
_guard: guard,
|
||||
crypto_policy,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -408,7 +419,7 @@ impl<'a> DbTxn<'a> {
|
|||
}
|
||||
|
||||
pub fn update_account_password(&self, a_uuid: &str, cred: &str) -> Result<(), ()> {
|
||||
let pw = Password::new(cred).map_err(|e| {
|
||||
let pw = Password::new(&self.crypto_policy, cred).map_err(|e| {
|
||||
error!("password error -> {:?}", e);
|
||||
})?;
|
||||
let dbpw = pw.to_dbpasswordv1();
|
||||
|
|
|
@ -4,12 +4,22 @@ use openssl::hash::MessageDigest;
|
|||
use openssl::pkcs5::pbkdf2_hmac;
|
||||
use rand::prelude::*;
|
||||
use std::convert::TryFrom;
|
||||
use std::time::{Duration, Instant};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub mod policy;
|
||||
pub mod totp;
|
||||
|
||||
use crate::credential::policy::CryptoPolicy;
|
||||
use crate::credential::totp::TOTP;
|
||||
|
||||
// NIST 800-63.b salt should be 112 bits -> 14 8u8.
|
||||
// I choose tinfoil hat though ...
|
||||
const PBKDF2_SALT_LEN: usize = 24;
|
||||
// 64 * u8 -> 512 bits of out.
|
||||
const PBKDF2_KEY_LEN: usize = 64;
|
||||
const PBKDF2_IMPORT_MIN_LEN: usize = 32;
|
||||
|
||||
// These are in order of "relative" strength.
|
||||
/*
|
||||
#[derive(Clone, Debug)]
|
||||
|
@ -21,16 +31,6 @@ pub enum Policy {
|
|||
}
|
||||
*/
|
||||
|
||||
// TODO #255: Determine this at startup based on a time factor
|
||||
const PBKDF2_COST: usize = 10000;
|
||||
// NIST 800-63.b salt should be 112 bits -> 14 8u8.
|
||||
// I choose tinfoil hat though ...
|
||||
const PBKDF2_SALT_LEN: usize = 24;
|
||||
// 64 * u8 -> 512 bits of out.
|
||||
const PBKDF2_KEY_LEN: usize = 64;
|
||||
|
||||
const PBKDF2_IMPORT_MIN_LEN: usize = 32;
|
||||
|
||||
// Why PBKDF2? Rust's bcrypt has a number of hardcodings like max pw len of 72
|
||||
// I don't really feel like adding in so many restrictions, so I'll use
|
||||
// pbkdf2 in openssl because it doesn't have the same limits.
|
||||
|
@ -94,7 +94,28 @@ impl TryFrom<&str> for Password {
|
|||
}
|
||||
|
||||
impl Password {
|
||||
fn new_pbkdf2(cleartext: &str) -> Result<KDF, OperationError> {
|
||||
fn bench_pbkdf2(pbkdf2_cost: usize) -> 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 start = Instant::now();
|
||||
let _ = pbkdf2_hmac(
|
||||
input.as_slice(),
|
||||
salt.as_slice(),
|
||||
pbkdf2_cost,
|
||||
MessageDigest::sha256(),
|
||||
key.as_mut_slice(),
|
||||
)
|
||||
.ok()?;
|
||||
let end = Instant::now();
|
||||
|
||||
end.checked_duration_since(start)
|
||||
}
|
||||
|
||||
fn new_pbkdf2(pbkdf2_cost: usize, cleartext: &str) -> Result<KDF, OperationError> {
|
||||
let mut rng = rand::thread_rng();
|
||||
let salt: Vec<u8> = (0..PBKDF2_SALT_LEN).map(|_| rng.gen()).collect();
|
||||
// This is 512 bits of output
|
||||
|
@ -103,19 +124,19 @@ impl Password {
|
|||
pbkdf2_hmac(
|
||||
cleartext.as_bytes(),
|
||||
salt.as_slice(),
|
||||
PBKDF2_COST,
|
||||
pbkdf2_cost,
|
||||
MessageDigest::sha256(),
|
||||
key.as_mut_slice(),
|
||||
)
|
||||
.map(|()| {
|
||||
// Turn key to a vec.
|
||||
KDF::PBKDF2(PBKDF2_COST, salt, key)
|
||||
KDF::PBKDF2(pbkdf2_cost, salt, key)
|
||||
})
|
||||
.map_err(|_| OperationError::CryptographyError)
|
||||
}
|
||||
|
||||
pub fn new(cleartext: &str) -> Result<Self, OperationError> {
|
||||
Self::new_pbkdf2(cleartext).map(|material| Password { material })
|
||||
pub fn new(policy: &CryptoPolicy, cleartext: &str) -> Result<Self, OperationError> {
|
||||
Self::new_pbkdf2(policy.pbkdf2_cost, cleartext).map(|material| Password { material })
|
||||
}
|
||||
|
||||
pub fn verify(&self, cleartext: &str) -> Result<bool, OperationError> {
|
||||
|
@ -211,8 +232,11 @@ impl TryFrom<DbCredV1> for Credential {
|
|||
}
|
||||
|
||||
impl Credential {
|
||||
pub fn new_password_only(cleartext: &str) -> Result<Self, OperationError> {
|
||||
Password::new(cleartext).map(|pw| Credential {
|
||||
pub fn new_password_only(
|
||||
policy: &CryptoPolicy,
|
||||
cleartext: &str,
|
||||
) -> Result<Self, OperationError> {
|
||||
Password::new(policy, cleartext).map(|pw| Credential {
|
||||
password: Some(pw),
|
||||
totp: None,
|
||||
claims: Vec::new(),
|
||||
|
@ -220,8 +244,12 @@ impl Credential {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn set_password(&self, cleartext: &str) -> Result<Self, OperationError> {
|
||||
Password::new(cleartext).map(|pw| Credential {
|
||||
pub fn set_password(
|
||||
&self,
|
||||
policy: &CryptoPolicy,
|
||||
cleartext: &str,
|
||||
) -> Result<Self, OperationError> {
|
||||
Password::new(policy, cleartext).map(|pw| Credential {
|
||||
password: Some(pw),
|
||||
totp: self.totp.clone(),
|
||||
claims: self.claims.clone(),
|
||||
|
@ -297,12 +325,14 @@ impl Credential {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::credential::policy::CryptoPolicy;
|
||||
use crate::credential::*;
|
||||
use std::convert::TryFrom;
|
||||
|
||||
#[test]
|
||||
fn test_credential_simple() {
|
||||
let c = Credential::new_password_only("password").unwrap();
|
||||
let p = CryptoPolicy::minimum();
|
||||
let c = Credential::new_password_only(&p, "password").unwrap();
|
||||
assert!(c.verify_password("password"));
|
||||
assert!(!c.verify_password("password1"));
|
||||
assert!(!c.verify_password("Password1"));
|
||||
|
|
47
kanidmd/src/lib/credential/policy.rs
Normal file
47
kanidmd/src/lib/credential/policy.rs
Normal file
|
@ -0,0 +1,47 @@
|
|||
use super::Password;
|
||||
use std::time::Duration;
|
||||
|
||||
const PBKDF2_MIN_NIST_COST: u64 = 10000;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CryptoPolicy {
|
||||
pub(crate) pbkdf2_cost: usize,
|
||||
}
|
||||
|
||||
impl CryptoPolicy {
|
||||
#[cfg(test)]
|
||||
pub(crate) fn minimum() -> Self {
|
||||
CryptoPolicy {
|
||||
pbkdf2_cost: PBKDF2_MIN_NIST_COST as usize,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn time_target(t: Duration) -> Self {
|
||||
let r = match Password::bench_pbkdf2((PBKDF2_MIN_NIST_COST * 10) as usize) {
|
||||
Some(bt) => {
|
||||
let ubt = bt.as_nanos() as u64;
|
||||
|
||||
// Get the cost per thousand rounds
|
||||
let per_thou = (PBKDF2_MIN_NIST_COST * 10) / 1000;
|
||||
let t_per_thou = ubt / per_thou;
|
||||
// eprintln!("{} / {}", ubt, per_thou);
|
||||
|
||||
// Now we need the attacker work in nanos
|
||||
let attack_time = t.as_nanos() as u64;
|
||||
let r = (attack_time / t_per_thou) * 1000;
|
||||
|
||||
// eprintln!("({} / {} ) * 1000", attack_time, t_per_thou);
|
||||
// eprintln!("Maybe rounds -> {}", r);
|
||||
|
||||
if r < PBKDF2_MIN_NIST_COST {
|
||||
PBKDF2_MIN_NIST_COST as usize
|
||||
} else {
|
||||
r as usize
|
||||
}
|
||||
}
|
||||
None => PBKDF2_MIN_NIST_COST as usize,
|
||||
};
|
||||
|
||||
CryptoPolicy { pbkdf2_cost: r }
|
||||
}
|
||||
}
|
|
@ -5,6 +5,7 @@ use kanidm_proto::v1::UserAuthToken;
|
|||
|
||||
use crate::audit::AuditScope;
|
||||
use crate::constants::UUID_ANONYMOUS;
|
||||
use crate::credential::policy::CryptoPolicy;
|
||||
use crate::credential::totp::TOTP;
|
||||
use crate::credential::Credential;
|
||||
use crate::idm::claim::Claim;
|
||||
|
@ -154,6 +155,7 @@ impl Account {
|
|||
&self,
|
||||
cleartext: &str,
|
||||
appid: &Option<String>,
|
||||
crypto_policy: &CryptoPolicy,
|
||||
) -> Result<ModifyList<ModifyInvalid>, OperationError> {
|
||||
// What should this look like? Probablf an appid + stuff -> modify?
|
||||
// then the caller has to apply the modify under the requests event
|
||||
|
@ -165,13 +167,13 @@ impl Account {
|
|||
match &self.primary {
|
||||
// Change the cred
|
||||
Some(primary) => {
|
||||
let ncred = primary.set_password(cleartext)?;
|
||||
let ncred = primary.set_password(crypto_policy, cleartext)?;
|
||||
let vcred = Value::new_credential("primary", ncred);
|
||||
Ok(ModifyList::new_purge_and_set("primary_credential", vcred))
|
||||
}
|
||||
// Make a new credential instead
|
||||
None => {
|
||||
let ncred = Credential::new_password_only(cleartext)?;
|
||||
let ncred = Credential::new_password_only(crypto_policy, cleartext)?;
|
||||
let vcred = Value::new_credential("primary", ncred);
|
||||
Ok(ModifyList::new_purge_and_set("primary_credential", vcred))
|
||||
}
|
||||
|
|
|
@ -406,6 +406,7 @@ impl AuthSession {
|
|||
mod tests {
|
||||
use crate::audit::AuditScope;
|
||||
use crate::constants::{JSON_ADMIN_V1, JSON_ANONYMOUS_V1};
|
||||
use crate::credential::policy::CryptoPolicy;
|
||||
use crate::credential::totp::{TOTP, TOTP_DEFAULT_STEP};
|
||||
use crate::credential::Credential;
|
||||
use crate::idm::authsession::{
|
||||
|
@ -492,7 +493,8 @@ mod tests {
|
|||
// create the ent
|
||||
let mut account = entry_str_to_account!(JSON_ADMIN_V1);
|
||||
// manually load in a cred
|
||||
let cred = Credential::new_password_only("test_password").unwrap();
|
||||
let p = CryptoPolicy::minimum();
|
||||
let cred = Credential::new_password_only(&p, "test_password").unwrap();
|
||||
account.primary = Some(cred);
|
||||
|
||||
// now check
|
||||
|
@ -549,7 +551,8 @@ mod tests {
|
|||
let pw_good = "test_password";
|
||||
let pw_bad = "bad_password";
|
||||
|
||||
let cred = Credential::new_password_only(pw_good)
|
||||
let p = CryptoPolicy::minimum();
|
||||
let cred = Credential::new_password_only(&p, pw_good)
|
||||
.unwrap()
|
||||
.update_totp(totp);
|
||||
// add totp also
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use crate::audit::AuditScope;
|
||||
use crate::constants::{AUTH_SESSION_TIMEOUT, MFAREG_SESSION_TIMEOUT, PW_MIN_LENGTH};
|
||||
use crate::constants::{UUID_ANONYMOUS, UUID_SYSTEM_CONFIG};
|
||||
use crate::credential::policy::CryptoPolicy;
|
||||
use crate::event::{AuthEvent, AuthEventStep, AuthResult};
|
||||
use crate::idm::account::Account;
|
||||
use crate::idm::authsession::AuthSession;
|
||||
|
@ -41,6 +42,9 @@ pub struct IdmServer {
|
|||
mfareg_sessions: BptreeMap<Uuid, MfaRegSession>,
|
||||
// Need a reference to the query server.
|
||||
qs: QueryServer,
|
||||
// The configured crypto policy for the IDM server. Later this could be transactional
|
||||
// and loaded from the db similar to access. But today it's just to allow dynamic pbkdf2rounds
|
||||
crypto_policy: CryptoPolicy,
|
||||
}
|
||||
|
||||
pub struct IdmServerWriteTransaction<'a> {
|
||||
|
@ -66,15 +70,25 @@ pub struct IdmServerProxyWriteTransaction<'a> {
|
|||
// Associate to an event origin ID, which has a TS and a UUID instead
|
||||
mfareg_sessions: BptreeMapWriteTxn<'a, Uuid, MfaRegSession>,
|
||||
sid: SID,
|
||||
crypto_policy: &'a CryptoPolicy,
|
||||
}
|
||||
|
||||
impl IdmServer {
|
||||
// TODO #59: Make number of authsessions configurable!!!
|
||||
pub fn new(qs: QueryServer) -> IdmServer {
|
||||
// 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));
|
||||
IdmServer {
|
||||
sessions: BptreeMap::new(),
|
||||
mfareg_sessions: BptreeMap::new(),
|
||||
qs,
|
||||
crypto_policy,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -106,6 +120,7 @@ impl IdmServer {
|
|||
mfareg_sessions: self.mfareg_sessions.write(),
|
||||
qs_write: self.qs.write(ts),
|
||||
sid,
|
||||
crypto_policy: &self.crypto_policy,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -507,7 +522,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
|||
|
||||
// it returns a modify
|
||||
let modlist = account
|
||||
.gen_password_mod(pce.cleartext.as_str(), &pce.appid)
|
||||
.gen_password_mod(pce.cleartext.as_str(), &pce.appid, self.crypto_policy)
|
||||
.map_err(|e| {
|
||||
ladmin_error!(au, "Failed to generate password mod {:?}", e);
|
||||
e
|
||||
|
@ -572,7 +587,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
|||
|
||||
// it returns a modify
|
||||
let modlist = account
|
||||
.gen_password_mod(pce.cleartext.as_str())
|
||||
.gen_password_mod(pce.cleartext.as_str(), self.crypto_policy)
|
||||
.map_err(|e| {
|
||||
ladmin_error!(au, "Unable to generate password change modlist {:?}", e);
|
||||
e
|
||||
|
@ -631,7 +646,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
|||
|
||||
// it returns a modify
|
||||
let modlist = account
|
||||
.gen_password_mod(cleartext.as_str(), &gpe.appid)
|
||||
.gen_password_mod(cleartext.as_str(), &gpe.appid, self.crypto_policy)
|
||||
.map_err(|e| {
|
||||
ladmin_error!(au, "Unable to generate password mod {:?}", e);
|
||||
e
|
||||
|
@ -797,6 +812,7 @@ mod tests {
|
|||
use crate::constants::{
|
||||
AUTH_SESSION_TIMEOUT, MFAREG_SESSION_TIMEOUT, UUID_ADMIN, UUID_ANONYMOUS,
|
||||
};
|
||||
use crate::credential::policy::CryptoPolicy;
|
||||
use crate::credential::totp::TOTP;
|
||||
use crate::credential::Credential;
|
||||
use crate::entry::{Entry, EntryInit, EntryNew};
|
||||
|
@ -941,7 +957,8 @@ mod tests {
|
|||
qs: &QueryServer,
|
||||
pw: &str,
|
||||
) -> Result<(), OperationError> {
|
||||
let cred = Credential::new_password_only(pw)?;
|
||||
let p = CryptoPolicy::minimum();
|
||||
let cred = Credential::new_password_only(&p, pw)?;
|
||||
let v_cred = Value::new_credential("primary", cred);
|
||||
let qs_write = qs.write(duration_from_epoch_now());
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ use uuid::Uuid;
|
|||
|
||||
use crate::audit::AuditScope;
|
||||
use crate::constants::UUID_ANONYMOUS;
|
||||
use crate::credential::policy::CryptoPolicy;
|
||||
use crate::credential::Credential;
|
||||
use crate::entry::{Entry, EntryCommitted, EntryReduced, EntrySealed};
|
||||
use crate::modify::{ModifyInvalid, ModifyList};
|
||||
|
@ -153,8 +154,9 @@ impl UnixUserAccount {
|
|||
pub(crate) fn gen_password_mod(
|
||||
&self,
|
||||
cleartext: &str,
|
||||
crypto_policy: &CryptoPolicy,
|
||||
) -> Result<ModifyList<ModifyInvalid>, OperationError> {
|
||||
let ncred = Credential::new_password_only(cleartext)?;
|
||||
let ncred = Credential::new_password_only(crypto_policy, cleartext)?;
|
||||
let vcred = Value::new_credential("unix", ncred);
|
||||
Ok(ModifyList::new_purge_and_set("unix_password", vcred))
|
||||
}
|
||||
|
|
|
@ -126,6 +126,7 @@ impl Plugin for PasswordImport {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::credential::policy::CryptoPolicy;
|
||||
use crate::credential::totp::{TOTP, TOTP_DEFAULT_STEP};
|
||||
use crate::credential::Credential;
|
||||
use crate::entry::{Entry, EntryInit, EntryNew};
|
||||
|
@ -205,7 +206,8 @@ mod tests {
|
|||
}"#,
|
||||
);
|
||||
|
||||
let c = Credential::new_password_only("password").unwrap();
|
||||
let p = CryptoPolicy::minimum();
|
||||
let c = Credential::new_password_only(&p, "password").unwrap();
|
||||
ea.add_ava("primary_credential", Value::new_credential("primary", c));
|
||||
|
||||
let preload = vec![ea];
|
||||
|
@ -239,7 +241,8 @@ mod tests {
|
|||
);
|
||||
|
||||
let totp = TOTP::generate_secure("test_totp".to_string(), TOTP_DEFAULT_STEP);
|
||||
let c = Credential::new_password_only("password")
|
||||
let p = CryptoPolicy::minimum();
|
||||
let c = Credential::new_password_only(&p, "password")
|
||||
.unwrap()
|
||||
.update_totp(totp);
|
||||
ea.add_ava("primary_credential", Value::new_credential("primary", c));
|
||||
|
|
|
@ -2363,6 +2363,7 @@ mod tests {
|
|||
JSON_SYSTEM_INFO_V1, RECYCLEBIN_MAX_AGE, SYSTEM_INDEX_VERSION, UUID_ADMIN,
|
||||
UUID_DOMAIN_INFO,
|
||||
};
|
||||
use crate::credential::policy::CryptoPolicy;
|
||||
use crate::credential::Credential;
|
||||
use crate::entry::{Entry, EntryInit, EntryNew};
|
||||
use crate::event::{CreateEvent, DeleteEvent, ModifyEvent, ReviveRecycledEvent, SearchEvent};
|
||||
|
@ -3447,7 +3448,8 @@ mod tests {
|
|||
assert!(cr.is_ok());
|
||||
|
||||
// Build the credential.
|
||||
let cred = Credential::new_password_only("test_password").unwrap();
|
||||
let p = CryptoPolicy::minimum();
|
||||
let cred = Credential::new_password_only(&p, "test_password").unwrap();
|
||||
let v_cred = Value::new_credential("primary", cred);
|
||||
assert!(v_cred.validate());
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
* cargo audit
|
||||
* cargo outdated
|
||||
|
||||
* upgrade crypto policy values if requires
|
||||
* bump index version in constants
|
||||
* check for breaking db entry changes.
|
||||
|
||||
|
|
Loading…
Reference in a new issue