Add support for multiple new password imports (#1100)

Woot!
This commit is contained in:
Firstyear 2022-10-10 06:32:04 +10:00 committed by GitHub
parent 31788e3733
commit 67c80f3fcb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 328 additions and 10 deletions

1
Cargo.lock generated
View file

@ -2335,6 +2335,7 @@ dependencies = [
"futures", "futures",
"futures-util", "futures-util",
"hashbrown", "hashbrown",
"hex",
"idlset", "idlset",
"kanidm_proto", "kanidm_proto",
"lazy_static", "lazy_static",

View file

@ -64,6 +64,7 @@ futures-util = "^0.3.21"
gloo = "^0.8.0" gloo = "^0.8.0"
gloo-net = "0.2.4" gloo-net = "0.2.4"
hashbrown = { version = "0.12.3", features = ["serde", "inline-more", "ahash"] } hashbrown = { version = "0.12.3", features = ["serde", "inline-more", "ahash"] }
hex = "^0.4.3"
http-types = "^2.12.0" http-types = "^2.12.0"
idlset = "^0.2.4" idlset = "^0.2.4"
# idlset = { path = "../idlset" } # idlset = { path = "../idlset" }

View file

@ -0,0 +1,25 @@
OTP tokens are stored in *related* entries:
Not sure how you would determine this in an import?
token key is base64 standard no pad
entryuuid: 94dbeb81-45f8-11ed-a50d-919b4b1a5ec0
syncstate: Add
dn: ipatokenuniqueid=16b6d328-cead-4b71-af5e-0a7b8622b204,cn=otp,dc=dev,dc=blackhats,dc=net,dc=au
ipatokenOTPalgorithm: sha1
ipatokenOTPdigits: 6
ipatokenOTPkey: iWM6XXyQt9GZV/yiPHYW/w4qjpaIJ8IlF6DlFndHe+C/zu4
ipatokenOwner: uid=testuser,cn=users,cn=accounts,dc=dev,dc=blackhats,dc=net,dc=au
ipatokenTOTPclockOffset: 0
ipatokenTOTPtimeStep: 30
ipatokenUniqueID: 16b6d328-cead-4b71-af5e-0a7b8622b204
objectClass: ipatoken
objectClass: ipatokentotp
objectClass: top

View file

@ -32,6 +32,7 @@ filetime.workspace = true
futures.workspace = true futures.workspace = true
futures-util.workspace = true futures-util.workspace = true
hashbrown.workspace = true hashbrown.workspace = true
hex.workspace = true
idlset.workspace = true idlset.workspace = true
kanidm_proto.workspace = true kanidm_proto.workspace = true
lazy_static.workspace = true lazy_static.workspace = true

View file

@ -21,16 +21,23 @@ pub struct DbCidV1 {
} }
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
#[allow(non_camel_case_types)]
pub enum DbPasswordV1 { pub enum DbPasswordV1 {
PBKDF2(usize, Vec<u8>, Vec<u8>), PBKDF2(usize, Vec<u8>, Vec<u8>),
PBKDF2_SHA1(usize, Vec<u8>, Vec<u8>),
PBKDF2_SHA512(usize, Vec<u8>, Vec<u8>),
SSHA512(Vec<u8>, Vec<u8>), SSHA512(Vec<u8>, Vec<u8>),
NT_MD4(Vec<u8>),
} }
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::PBKDF2(_, _, _) => write!(f, "PBKDF2"), DbPasswordV1::PBKDF2(_, _, _) => write!(f, "PBKDF2"),
DbPasswordV1::PBKDF2_SHA1(_, _, _) => write!(f, "PBKDF2_SHA1"),
DbPasswordV1::PBKDF2_SHA512(_, _, _) => write!(f, "PBKDF2_SHA512"),
DbPasswordV1::SSHA512(_, _) => write!(f, "SSHA512"), DbPasswordV1::SSHA512(_, _) => write!(f, "SSHA512"),
DbPasswordV1::NT_MD4(_) => write!(f, "NT_MD4"),
} }
} }
} }

View file

@ -3,7 +3,8 @@ use std::time::{Duration, Instant};
use hashbrown::{HashMap as Map, HashSet}; use hashbrown::{HashMap as Map, HashSet};
use kanidm_proto::v1::{BackupCodesView, CredentialDetail, CredentialDetailType, OperationError}; use kanidm_proto::v1::{BackupCodesView, CredentialDetail, CredentialDetailType, OperationError};
use openssl::hash::MessageDigest; use openssl::hash::{self, MessageDigest};
use openssl::nid::Nid;
use openssl::pkcs5::pbkdf2_hmac; use openssl::pkcs5::pbkdf2_hmac;
use openssl::sha::Sha512; use openssl::sha::Sha512;
use rand::prelude::*; use rand::prelude::*;
@ -24,9 +25,16 @@ use crate::credential::totp::Totp;
// 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 ... // 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;
// Min number of rounds for a pbkdf2
pub const PBKDF2_MIN_NIST_COST: usize = 10000;
// 64 * u8 -> 512 bits of out. // 64 * u8 -> 512 bits of out.
const PBKDF2_KEY_LEN: usize = 64; const PBKDF2_KEY_LEN: usize = 64;
const PBKDF2_IMPORT_MIN_LEN: usize = 32; const PBKDF2_MIN_NIST_KEY_LEN: usize = 32;
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;
@ -46,11 +54,20 @@ pub enum Policy {
// I don't really feel like adding in so many restrictions, so I'll use // 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. // pbkdf2 in openssl because it doesn't have the same limits.
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
#[allow(non_camel_case_types)]
enum Kdf { enum Kdf {
// cost, salt, hash // cost, salt, hash
PBKDF2(usize, Vec<u8>, Vec<u8>), PBKDF2(usize, Vec<u8>, Vec<u8>),
// Imported types, will upgrade to the above.
// cost, salt, hash
PBKDF2_SHA1(usize, Vec<u8>, Vec<u8>),
// cost, salt, hash
PBKDF2_SHA512(usize, Vec<u8>, Vec<u8>),
// salt hash // salt hash
SSHA512(Vec<u8>, Vec<u8>), SSHA512(Vec<u8>, Vec<u8>),
// hash
NT_MD4(Vec<u8>),
} }
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
@ -66,13 +83,45 @@ impl TryFrom<DbPasswordV1> for Password {
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),
}), }),
DbPasswordV1::PBKDF2_SHA1(c, s, h) => Ok(Password {
material: Kdf::PBKDF2_SHA1(c, s, h),
}),
DbPasswordV1::PBKDF2_SHA512(c, s, h) => Ok(Password {
material: Kdf::PBKDF2_SHA512(c, s, h),
}),
DbPasswordV1::SSHA512(s, h) => Ok(Password { DbPasswordV1::SSHA512(s, h) => Ok(Password {
material: Kdf::SSHA512(s, h), material: Kdf::SSHA512(s, h),
}), }),
DbPasswordV1::NT_MD4(h) => Ok(Password {
material: Kdf::NT_MD4(h),
}),
} }
} }
} }
// OpenLDAP based their PBKDF2 implementation on passlib from python, that uses a
// non-standard base64 altchar set and padding that is not supported by
// anything else in the world. To manage this, we only ever encode to base64 with
// no pad but we have to remap ab64 to b64. This function allows b64 standard with
// padding to pass, and remaps ab64 to b64 standard with padding.
macro_rules! ab64_to_b64 {
($ab64:expr) => {{
let mut s = $ab64.replace(".", "+");
match s.len() & 3 {
0 => {
// Do nothing
}
1 => {
// One is invalid, do nothing, we'll error in base64
}
2 => s.push_str("=="),
3 => s.push_str("="),
_ => unreachable!(),
}
s
}};
}
impl TryFrom<&str> for Password { impl TryFrom<&str> for Password {
type Error = (); type Error = ();
@ -93,7 +142,7 @@ impl TryFrom<&str> for Password {
let c = cost.parse::<usize>().map_err(|_| ())?; let c = cost.parse::<usize>().map_err(|_| ())?;
let s: Vec<_> = salt.as_bytes().to_vec(); let s: Vec<_> = salt.as_bytes().to_vec();
let h = base64::decode(hash).map_err(|_| ())?; let h = base64::decode(hash).map_err(|_| ())?;
if h.len() < PBKDF2_IMPORT_MIN_LEN { if h.len() < PBKDF2_MIN_NIST_KEY_LEN {
return Err(()); return Err(());
} }
return Ok(Password { return Ok(Password {
@ -104,6 +153,34 @@ impl TryFrom<&str> for Password {
} }
} }
if value.starts_with("ipaNTHash: ") {
let nt_md4 = match value.split_once(" ") {
Some((_, v)) => v,
None => {
unreachable!();
}
};
let h = base64::decode_config(nt_md4, base64::STANDARD_NO_PAD).map_err(|_| ())?;
return Ok(Password {
material: Kdf::NT_MD4(h),
});
}
if value.starts_with("sambaNTPassword: ") {
let nt_md4 = match value.split_once(" ") {
Some((_, v)) => v,
None => {
unreachable!();
}
};
let h = hex::decode(nt_md4).map_err(|_| ())?;
return Ok(Password {
material: Kdf::NT_MD4(h),
});
}
// Test 389ds formats // Test 389ds formats
if let Some(ds_ssha512) = value.strip_prefix("{SSHA512}") { if let Some(ds_ssha512) = value.strip_prefix("{SSHA512}") {
let sh = base64::decode(ds_ssha512).map_err(|_| ())?; let sh = base64::decode(ds_ssha512).map_err(|_| ())?;
@ -116,6 +193,78 @@ impl TryFrom<&str> for Password {
}); });
} }
// Test for OpenLDAP formats
if value.starts_with("{PBKDF2}")
|| value.starts_with("{PBKDF2-SHA1}")
|| value.starts_with("{PBKDF2-SHA256}")
|| value.starts_with("{PBKDF2-SHA512}")
{
let ol_pbkdf2 = match value.split_once("}") {
Some((_, v)) => v,
None => {
unreachable!();
}
};
let ol_pbkdf: Vec<&str> = ol_pbkdf2.split('$').collect();
if ol_pbkdf.len() == 3 {
let cost = ol_pbkdf[0];
let salt = ol_pbkdf[1];
let hash = ol_pbkdf[2];
let c = cost.parse::<usize>().map_err(|_| ())?;
let s = ab64_to_b64!(salt);
let s =
base64::decode_config(&s, base64::STANDARD.decode_allow_trailing_bits(true))
.map_err(|e| {
error!(?e, "Invalid base64 in oldap pbkdf2-sha1");
})?;
let h = ab64_to_b64!(hash);
let h =
base64::decode_config(&h, base64::STANDARD.decode_allow_trailing_bits(true))
.map_err(|e| {
error!(?e, "Invalid base64 in oldap pbkdf2-sha1");
})?;
// This is just sha1 in a trenchcoat.
if value.strip_prefix("{PBKDF2}").is_some()
|| value.strip_prefix("{PBKDF2-SHA1}").is_some()
{
if h.len() < PBKDF2_SHA1_MIN_KEY_LEN {
return Err(());
}
return Ok(Password {
material: Kdf::PBKDF2_SHA1(c, s, h),
});
}
if value.strip_prefix("{PBKDF2-SHA256}").is_some() {
if h.len() < PBKDF2_MIN_NIST_KEY_LEN {
return Err(());
}
return Ok(Password {
material: Kdf::PBKDF2(c, s, h),
});
}
if value.strip_prefix("{PBKDF2-SHA512}").is_some() {
if h.len() < PBKDF2_MIN_NIST_KEY_LEN {
return Err(());
}
return Ok(Password {
material: Kdf::PBKDF2_SHA512(c, s, h),
});
}
// Should be no way to get here!
unreachable!();
} else {
warn!("oldap pbkdf2 found but invalid number of elements?");
}
}
// Nothing matched to this point. // Nothing matched to this point.
Err(()) Err(())
} }
@ -173,7 +322,7 @@ impl Password {
// 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
let key_len = key.len(); let key_len = key.len();
debug_assert!(key_len >= PBKDF2_IMPORT_MIN_LEN); debug_assert!(key_len >= PBKDF2_MIN_NIST_KEY_LEN);
let mut chal_key: Vec<u8> = (0..key_len).map(|_| 0).collect(); let mut chal_key: Vec<u8> = (0..key_len).map(|_| 0).collect();
pbkdf2_hmac( pbkdf2_hmac(
cleartext.as_bytes(), cleartext.as_bytes(),
@ -188,6 +337,40 @@ impl Password {
&chal_key == key &chal_key == key
}) })
} }
Kdf::PBKDF2_SHA1(cost, salt, key) => {
let key_len = key.len();
debug_assert!(key_len >= PBKDF2_SHA1_MIN_KEY_LEN);
let mut chal_key: Vec<u8> = (0..key_len).map(|_| 0).collect();
pbkdf2_hmac(
cleartext.as_bytes(),
salt.as_slice(),
*cost,
MessageDigest::sha1(),
chal_key.as_mut_slice(),
)
.map_err(|_| OperationError::CryptographyError)
.map(|()| {
// Actually compare the outputs.
&chal_key == key
})
}
Kdf::PBKDF2_SHA512(cost, salt, key) => {
let key_len = key.len();
debug_assert!(key_len >= PBKDF2_MIN_NIST_KEY_LEN);
let mut chal_key: Vec<u8> = (0..key_len).map(|_| 0).collect();
pbkdf2_hmac(
cleartext.as_bytes(),
salt.as_slice(),
*cost,
MessageDigest::sha512(),
chal_key.as_mut_slice(),
)
.map_err(|_| OperationError::CryptographyError)
.map(|()| {
// Actually compare the outputs.
&chal_key == key
})
}
Kdf::SSHA512(salt, key) => { Kdf::SSHA512(salt, key) => {
let mut hasher = Sha512::new(); let mut hasher = Sha512::new();
hasher.update(cleartext.as_bytes()); hasher.update(cleartext.as_bytes());
@ -195,6 +378,23 @@ impl Password {
let r = hasher.finish(); let r = hasher.finish();
Ok(key == &(r.to_vec())) Ok(key == &(r.to_vec()))
} }
Kdf::NT_MD4(key) => {
// We need to get the cleartext to utf16le for reasons.
let clear_utf16le: Vec<u8> = cleartext
.encode_utf16()
.map(|c| c.to_le_bytes())
.flat_map(|i| i.into_iter())
.collect();
let dgst = MessageDigest::from_nid(Nid::MD4).ok_or_else(|| {
error!("Unable to access MD4 - fips mode enabled?");
OperationError::CryptographyError
})?;
hash::hash(dgst, &clear_utf16le)
.map_err(|_| OperationError::CryptographyError)
.map(|chal_key| chal_key.as_ref() == key)
}
} }
} }
@ -203,12 +403,26 @@ impl Password {
Kdf::PBKDF2(cost, salt, hash) => { Kdf::PBKDF2(cost, salt, hash) => {
DbPasswordV1::PBKDF2(*cost, salt.clone(), hash.clone()) DbPasswordV1::PBKDF2(*cost, salt.clone(), hash.clone())
} }
Kdf::PBKDF2_SHA1(cost, salt, hash) => {
DbPasswordV1::PBKDF2_SHA1(*cost, salt.clone(), hash.clone())
}
Kdf::PBKDF2_SHA512(cost, salt, hash) => {
DbPasswordV1::PBKDF2_SHA512(*cost, salt.clone(), hash.clone())
}
Kdf::SSHA512(salt, hash) => DbPasswordV1::SSHA512(salt.clone(), hash.clone()), Kdf::SSHA512(salt, hash) => DbPasswordV1::SSHA512(salt.clone(), hash.clone()),
Kdf::NT_MD4(hash) => DbPasswordV1::NT_MD4(hash.clone()),
} }
} }
pub fn requires_upgrade(&self) -> bool { pub fn requires_upgrade(&self) -> bool {
!matches!(&self.material, Kdf::PBKDF2(_, _, _)) match &self.material {
Kdf::PBKDF2_SHA512(cost, salt, hash) | Kdf::PBKDF2(cost, salt, hash) => {
*cost < PBKDF2_MIN_NIST_COST
|| salt.len() < PBKDF2_MIN_NIST_SALT_LEN
|| hash.len() < PBKDF2_MIN_NIST_KEY_LEN
}
Kdf::PBKDF2_SHA1(_, _, _) | Kdf::SSHA512(_, _) | Kdf::NT_MD4(_) => true,
}
} }
} }
@ -930,4 +1144,75 @@ mod tests {
assert!(r.requires_upgrade()); assert!(r.requires_upgrade());
assert!(r.verify(password).unwrap_or(false)); assert!(r.verify(password).unwrap_or(false));
} }
// Can be generated with:
// slappasswd -s password -o module-load=/usr/lib64/openldap/pw-argon2.so -h {ARGON2}
#[test]
fn test_password_from_openldap_pkbdf2() {
let im_pw = "{PBKDF2}10000$IlfapjA351LuDSwYC0IQ8Q$saHqQTuYnjJN/tmAndT.8mJt.6w";
let password = "password";
let r = Password::try_from(im_pw).expect("Failed to parse");
assert!(r.requires_upgrade());
assert!(r.verify(password).unwrap_or(false));
}
#[test]
fn test_password_from_openldap_pkbdf2_sha1() {
let im_pw = "{PBKDF2-SHA1}10000$ZBEH6B07rgQpJSikyvMU2w$TAA03a5IYkz1QlPsbJKvUsTqNV";
let password = "password";
let r = Password::try_from(im_pw).expect("Failed to parse");
assert!(r.requires_upgrade());
assert!(r.verify(password).unwrap_or(false));
}
#[test]
fn test_password_from_openldap_pkbdf2_sha256() {
let im_pw = "{PBKDF2-SHA256}10000$henZGfPWw79Cs8ORDeVNrQ$1dTJy73v6n3bnTmTZFghxHXHLsAzKaAy8SksDfZBPIw";
let password = "password";
let r = Password::try_from(im_pw).expect("Failed to parse");
assert!(!r.requires_upgrade());
assert!(r.verify(password).unwrap_or(false));
}
#[test]
fn test_password_from_openldap_pkbdf2_sha512() {
let im_pw = "{PBKDF2-SHA512}10000$Je1Uw19Bfv5lArzZ6V3EPw$g4T/1sqBUYWl9o93MVnyQ/8zKGSkPbKaXXsT8WmysXQJhWy8MRP2JFudSL.N9RklQYgDPxPjnfum/F2f/TrppA";
let password = "password";
let r = Password::try_from(im_pw).expect("Failed to parse");
assert!(!r.requires_upgrade());
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"
let password = "password";
let r = Password::try_from(im_pw).expect("Failed to parse");
assert!(r.requires_upgrade());
assert!(r.verify(password).unwrap_or(false));
}
*/
#[test]
fn test_password_from_ipa_nt_hash() {
// Base64 no pad
let im_pw = "ipaNTHash: iEb36u6PsRetBr3YMLdYbA";
let password = "password";
let r = Password::try_from(im_pw).expect("Failed to parse");
assert!(r.requires_upgrade());
assert!(r.verify(password).unwrap_or(false));
}
#[test]
fn test_password_from_samba_nt_hash() {
// Base64 no pad
let im_pw = "sambaNTPassword: 8846F7EAEE8FB117AD06BDD830B7586C";
let password = "password";
let r = Password::try_from(im_pw).expect("Failed to parse");
assert!(r.requires_upgrade());
assert!(r.verify(password).unwrap_or(false));
}
} }

View file

@ -1,8 +1,6 @@
use std::time::Duration; use std::time::Duration;
use super::Password; use super::{Password, PBKDF2_MIN_NIST_COST};
const PBKDF2_MIN_NIST_COST: u64 = 10000;
#[derive(Debug)] #[derive(Debug)]
pub struct CryptoPolicy { pub struct CryptoPolicy {
@ -20,7 +18,7 @@ impl CryptoPolicy {
pub fn time_target(t: Duration) -> Self { pub fn time_target(t: Duration) -> Self {
let r = match Password::bench_pbkdf2((PBKDF2_MIN_NIST_COST * 10) as usize) { let r = match Password::bench_pbkdf2((PBKDF2_MIN_NIST_COST * 10) as usize) {
Some(bt) => { Some(bt) => {
let ubt = bt.as_nanos() as u64; 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 * 10) / 1000;
@ -28,7 +26,7 @@ impl CryptoPolicy {
// eprintln!("{} / {}", ubt, per_thou); // eprintln!("{} / {}", ubt, 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 u64; let attack_time = t.as_nanos() as usize;
let r = (attack_time / t_per_thou) * 1000; let r = (attack_time / t_per_thou) * 1000;
// eprintln!("({} / {} ) * 1000", attack_time, t_per_thou); // eprintln!("({} / {} ) * 1000", attack_time, t_per_thou);