Dynamic crypto rounds (#311)

This commit is contained in:
Firstyear 2020-08-17 11:26:28 +10:00 committed by GitHub
parent e34a848a88
commit bd8d2af420
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 152 additions and 34 deletions

View file

@ -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();

View file

@ -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"));

View 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 }
}
}

View file

@ -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))
}

View file

@ -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

View file

@ -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());

View file

@ -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))
}

View file

@ -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));

View file

@ -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());

View file

@ -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.