From 0e0e8ff844fe133d518ea007f1a971ab1a549a1c Mon Sep 17 00:00:00 2001 From: Firstyear <william@blackhats.net.au> Date: Tue, 25 Feb 2025 21:09:34 +1000 Subject: [PATCH] Add crypt formats for password import (#3458) Adds crypt md5, sha256 and sha512 allowing import of legacy credentials from external ldap servers. --- Cargo.lock | 12 ++ Cargo.toml | 1 + libs/crypto/Cargo.toml | 3 + libs/crypto/src/crypt_md5.rs | 99 ++++++++++ libs/crypto/src/lib.rs | 337 +++++++++++++---------------------- 5 files changed, 242 insertions(+), 210 deletions(-) create mode 100644 libs/crypto/src/crypt_md5.rs diff --git a/Cargo.lock b/Cargo.lock index e049c4274..c61a05c3f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2978,10 +2978,12 @@ dependencies = [ "hex", "kanidm-hsm-crypto", "kanidm_proto", + "md-5", "openssl", "openssl-sys", "rand", "serde", + "sha-crypt", "sha2", "sketching", "tracing", @@ -3528,6 +3530,16 @@ dependencies = [ "rand", ] +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + [[package]] name = "memchr" version = "2.7.4" diff --git a/Cargo.toml b/Cargo.toml index 9d0634dd1..588241cc1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -204,6 +204,7 @@ libsqlite3-sys = "^0.25.2" lodepng = "3.11.0" lru = "^0.12.5" mathru = "^0.13.0" +md-5 = "0.10.6" mimalloc = "0.1.43" notify-debouncer-full = { version = "0.1" } num_enum = "^0.5.11" diff --git a/libs/crypto/Cargo.toml b/libs/crypto/Cargo.toml index 854f127a9..1dc58b360 100644 --- a/libs/crypto/Cargo.toml +++ b/libs/crypto/Cargo.toml @@ -33,6 +33,9 @@ tracing = { workspace = true } uuid = { workspace = true } x509-cert = { workspace = true, features = ["pem"] } +md-5 = { workspace = true } +sha-crypt = { workspace = true } + [dev-dependencies] sketching = { workspace = true } diff --git a/libs/crypto/src/crypt_md5.rs b/libs/crypto/src/crypt_md5.rs new file mode 100644 index 000000000..9a5120048 --- /dev/null +++ b/libs/crypto/src/crypt_md5.rs @@ -0,0 +1,99 @@ +use md5::{Digest, Md5}; +use std::cmp::min; + +/// Maximium salt length. +const MD5_MAGIC: &str = "$1$"; +const MD5_TRANSPOSE: &[u8] = b"\x0c\x06\x00\x0d\x07\x01\x0e\x08\x02\x0f\x09\x03\x05\x0a\x04\x0b"; + +const CRYPT_HASH64: &[u8] = b"./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + +pub fn md5_sha2_hash64_encode(bs: &[u8]) -> String { + let ngroups = (bs.len() + 2) / 3; + let mut out = String::with_capacity(ngroups * 4); + for g in 0..ngroups { + let mut g_idx = g * 3; + let mut enc = 0u32; + for _ in 0..3 { + let b = (if g_idx < bs.len() { bs[g_idx] } else { 0 }) as u32; + enc >>= 8; + enc |= b << 16; + g_idx += 1; + } + for _ in 0..4 { + out.push(char::from_u32(CRYPT_HASH64[(enc & 0x3F) as usize] as u32).unwrap_or('!')); + enc >>= 6; + } + } + match bs.len() % 3 { + 1 => { + out.pop(); + out.pop(); + } + 2 => { + out.pop(); + } + _ => (), + } + out +} + +pub fn do_md5_crypt(pass: &[u8], salt: &[u8]) -> Vec<u8> { + let mut dgst_b = Md5::new(); + dgst_b.update(pass); + dgst_b.update(salt); + dgst_b.update(pass); + let mut hash_b = dgst_b.finalize(); + + let mut dgst_a = Md5::new(); + dgst_a.update(pass); + dgst_a.update(MD5_MAGIC.as_bytes()); + dgst_a.update(salt); + + let mut plen = pass.len(); + while plen > 0 { + dgst_a.update(&hash_b[..min(plen, 16)]); + if plen < 16 { + break; + } + plen -= 16; + } + + plen = pass.len(); + while plen > 0 { + if plen & 1 == 0 { + dgst_a.update(&pass[..1]) + } else { + dgst_a.update([0u8]) + } + plen >>= 1; + } + + let mut hash_a = dgst_a.finalize(); + + for r in 0..1000 { + let mut dgst_a = Md5::new(); + if r % 2 == 1 { + dgst_a.update(pass); + } else { + dgst_a.update(hash_a); + } + if r % 3 > 0 { + dgst_a.update(salt); + } + if r % 7 > 0 { + dgst_a.update(pass); + } + if r % 2 == 0 { + dgst_a.update(pass); + } else { + dgst_a.update(hash_a); + } + hash_a = dgst_a.finalize(); + } + + for (i, &ti) in MD5_TRANSPOSE.iter().enumerate() { + hash_b[i] = hash_a[ti as usize]; + } + + md5_sha2_hash64_encode(&hash_b).into_bytes() +} diff --git a/libs/crypto/src/lib.rs b/libs/crypto/src/lib.rs index 7e79f7949..f95b2ae73 100644 --- a/libs/crypto/src/lib.rs +++ b/libs/crypto/src/lib.rs @@ -11,26 +11,24 @@ #![deny(clippy::unreachable)] use argon2::{Algorithm, Argon2, Params, PasswordHash, Version}; +use base64::engine::general_purpose; use base64::engine::GeneralPurpose; use base64::{alphabet, Engine}; -use tracing::{debug, error, trace, warn}; - -use base64::engine::general_purpose; use base64urlsafedata::Base64UrlSafeData; -use rand::Rng; -use serde::{Deserialize, Serialize}; -use std::fmt; -use std::time::{Duration, Instant}; - +use kanidm_hsm_crypto::{HmacKey, Tpm}; use kanidm_proto::internal::OperationError; use openssl::error::ErrorStack as OpenSSLErrorStack; use openssl::hash::{self, MessageDigest}; use openssl::nid::Nid; use openssl::pkcs5::pbkdf2_hmac; use openssl::sha::{Sha1, Sha256, Sha512}; +use rand::Rng; +use serde::{Deserialize, Serialize}; +use std::fmt; +use std::time::{Duration, Instant}; +use tracing::{debug, error, trace, warn}; -use kanidm_hsm_crypto::{HmacKey, Tpm}; - +mod crypt_md5; pub mod mtls; pub mod prelude; pub mod serialise; @@ -84,6 +82,7 @@ pub enum CryptoError { Argon2, Argon2Version, Argon2Parameters, + Crypt, } impl From<OpenSSLErrorStack> for CryptoError { @@ -137,65 +136,15 @@ pub enum DbPasswordV1 { SHA512(Vec<u8>), SSHA512(Vec<u8>, Vec<u8>), NT_MD4(Vec<u8>), -} - -#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] -#[allow(non_camel_case_types)] -pub enum ReplPasswordV1 { - TPM_ARGON2ID { - m_cost: u32, - t_cost: u32, - p_cost: u32, - version: u32, - salt: Base64UrlSafeData, - key: Base64UrlSafeData, + CRYPT_MD5 { + s: Base64UrlSafeData, + h: Base64UrlSafeData, }, - ARGON2ID { - m_cost: u32, - t_cost: u32, - p_cost: u32, - version: u32, - salt: Base64UrlSafeData, - key: Base64UrlSafeData, + CRYPT_SHA256 { + h: String, }, - PBKDF2 { - cost: usize, - salt: Base64UrlSafeData, - hash: Base64UrlSafeData, - }, - PBKDF2_SHA1 { - cost: usize, - salt: Base64UrlSafeData, - hash: Base64UrlSafeData, - }, - PBKDF2_SHA512 { - cost: usize, - salt: Base64UrlSafeData, - hash: Base64UrlSafeData, - }, - SHA1 { - hash: Base64UrlSafeData, - }, - SSHA1 { - salt: Base64UrlSafeData, - hash: Base64UrlSafeData, - }, - SHA256 { - hash: Base64UrlSafeData, - }, - SSHA256 { - salt: Base64UrlSafeData, - hash: Base64UrlSafeData, - }, - SHA512 { - hash: Base64UrlSafeData, - }, - SSHA512 { - salt: Base64UrlSafeData, - hash: Base64UrlSafeData, - }, - NT_MD4 { - hash: Base64UrlSafeData, + CRYPT_SHA512 { + h: String, }, } @@ -214,6 +163,9 @@ impl fmt::Debug for DbPasswordV1 { DbPasswordV1::SHA512(_) => write!(f, "SHA512"), DbPasswordV1::SSHA512(_, _) => write!(f, "SSHA512"), DbPasswordV1::NT_MD4(_) => write!(f, "NT_MD4"), + DbPasswordV1::CRYPT_MD5 { .. } => write!(f, "CRYPT_MD5"), + DbPasswordV1::CRYPT_SHA256 { .. } => write!(f, "CRYPT_SHA256"), + DbPasswordV1::CRYPT_SHA512 { .. } => write!(f, "CRYPT_SHA512"), } } } @@ -436,6 +388,16 @@ enum Kdf { SSHA512(Vec<u8>, Vec<u8>), // hash NT_MD4(Vec<u8>), + CRYPT_MD5 { + s: Vec<u8>, + h: Vec<u8>, + }, + CRYPT_SHA256 { + h: String, + }, + CRYPT_SHA512 { + h: String, + }, } #[derive(Clone, Debug, PartialEq)] @@ -498,78 +460,17 @@ impl TryFrom<DbPasswordV1> for Password { DbPasswordV1::NT_MD4(h) => Ok(Password { material: Kdf::NT_MD4(h), }), - } - } -} - -impl TryFrom<&ReplPasswordV1> for Password { - type Error = (); - - fn try_from(value: &ReplPasswordV1) -> Result<Self, Self::Error> { - match value { - ReplPasswordV1::TPM_ARGON2ID { - m_cost, - t_cost, - p_cost, - version, - salt, - key, - } => Ok(Password { - material: Kdf::TPM_ARGON2ID { - m_cost: *m_cost, - t_cost: *t_cost, - p_cost: *p_cost, - version: *version, - salt: salt.to_vec(), - key: key.to_vec(), + DbPasswordV1::CRYPT_MD5 { s, h } => Ok(Password { + material: Kdf::CRYPT_MD5 { + s: s.into(), + h: h.into(), }, }), - 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.to_vec(), - key: key.to_vec(), - }, + DbPasswordV1::CRYPT_SHA256 { h } => Ok(Password { + material: Kdf::CRYPT_SHA256 { h }, }), - ReplPasswordV1::PBKDF2 { cost, salt, hash } => Ok(Password { - material: Kdf::PBKDF2(*cost, salt.to_vec(), hash.to_vec()), - }), - ReplPasswordV1::PBKDF2_SHA1 { cost, salt, hash } => Ok(Password { - material: Kdf::PBKDF2_SHA1(*cost, salt.to_vec(), hash.to_vec()), - }), - ReplPasswordV1::PBKDF2_SHA512 { cost, salt, hash } => Ok(Password { - material: Kdf::PBKDF2_SHA512(*cost, salt.to_vec(), hash.to_vec()), - }), - ReplPasswordV1::SHA1 { hash } => Ok(Password { - material: Kdf::SHA1(hash.to_vec()), - }), - ReplPasswordV1::SSHA1 { salt, hash } => Ok(Password { - material: Kdf::SSHA1(salt.to_vec(), hash.to_vec()), - }), - ReplPasswordV1::SHA256 { hash } => Ok(Password { - material: Kdf::SHA256(hash.to_vec()), - }), - ReplPasswordV1::SSHA256 { salt, hash } => Ok(Password { - material: Kdf::SSHA256(salt.to_vec(), hash.to_vec()), - }), - ReplPasswordV1::SHA512 { hash } => Ok(Password { - material: Kdf::SHA512(hash.to_vec()), - }), - ReplPasswordV1::SSHA512 { salt, hash } => Ok(Password { - material: Kdf::SSHA512(salt.to_vec(), hash.to_vec()), - }), - ReplPasswordV1::NT_MD4 { hash } => Ok(Password { - material: Kdf::NT_MD4(hash.to_vec()), + DbPasswordV1::CRYPT_SHA512 { h } => Ok(Password { + material: Kdf::CRYPT_SHA256 { h }, }), } } @@ -665,6 +566,40 @@ impl TryFrom<&str> for Password { // Test 389ds/openldap formats. Shout outs openldap which sometimes makes these // lowercase. + if let Some(crypt) = value + .strip_prefix("{crypt}") + .or_else(|| value.strip_prefix("{CRYPT}")) + { + if let Some(crypt_md5_phc) = crypt.strip_prefix("$1$") { + let (salt, hash) = crypt_md5_phc.split_once('$').ok_or(())?; + + // These are a hash64 format, so leave them as bytes, don't try + // to decode. + let s = salt.as_bytes().to_vec(); + let h = hash.as_bytes().to_vec(); + + return Ok(Password { + material: Kdf::CRYPT_MD5 { s, h }, + }); + } + + if crypt.starts_with("$5$") { + return Ok(Password { + material: Kdf::CRYPT_SHA256 { + h: crypt.to_string(), + }, + }); + } + + if crypt.starts_with("$6$") { + return Ok(Password { + material: Kdf::CRYPT_SHA512 { + h: crypt.to_string(), + }, + }); + } + } // End crypt + if let Some(ds_ssha1) = value .strip_prefix("{SHA}") .or_else(|| value.strip_prefix("{sha}")) @@ -1242,6 +1177,20 @@ impl Password { }) .map(|chal_key| chal_key.as_ref() == key) } + (Kdf::CRYPT_MD5 { s, h }, _) => { + let chal_key = crypt_md5::do_md5_crypt(cleartext.as_bytes(), s); + Ok(chal_key == *h) + } + (Kdf::CRYPT_SHA256 { h }, _) => { + let is_valid = sha_crypt::sha256_check(cleartext, h.as_str()).is_ok(); + + Ok(is_valid) + } + (Kdf::CRYPT_SHA512 { h }, _) => { + let is_valid = sha_crypt::sha512_check(cleartext, h.as_str()).is_ok(); + + Ok(is_valid) + } } } @@ -1293,80 +1242,12 @@ impl Password { Kdf::SHA512(hash) => DbPasswordV1::SHA512(hash.clone()), Kdf::SSHA512(salt, hash) => DbPasswordV1::SSHA512(salt.clone(), hash.clone()), Kdf::NT_MD4(hash) => DbPasswordV1::NT_MD4(hash.clone()), - } - } - - pub fn to_repl_v1(&self) -> ReplPasswordV1 { - match &self.material { - Kdf::TPM_ARGON2ID { - m_cost, - t_cost, - p_cost, - version, - salt, - key, - } => ReplPasswordV1::TPM_ARGON2ID { - m_cost: *m_cost, - t_cost: *t_cost, - p_cost: *p_cost, - version: *version, - salt: salt.clone().into(), - key: key.clone().into(), - }, - 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(), - hash: hash.clone().into(), - }, - Kdf::PBKDF2_SHA1(cost, salt, hash) => ReplPasswordV1::PBKDF2_SHA1 { - cost: *cost, - salt: salt.clone().into(), - hash: hash.clone().into(), - }, - Kdf::PBKDF2_SHA512(cost, salt, hash) => ReplPasswordV1::PBKDF2_SHA512 { - cost: *cost, - salt: salt.clone().into(), - hash: hash.clone().into(), - }, - Kdf::SHA1(hash) => ReplPasswordV1::SHA1 { - hash: hash.clone().into(), - }, - Kdf::SSHA1(salt, hash) => ReplPasswordV1::SSHA1 { - salt: salt.clone().into(), - hash: hash.clone().into(), - }, - Kdf::SHA256(hash) => ReplPasswordV1::SHA256 { - hash: hash.clone().into(), - }, - Kdf::SSHA256(salt, hash) => ReplPasswordV1::SSHA256 { - salt: salt.clone().into(), - hash: hash.clone().into(), - }, - Kdf::SHA512(hash) => ReplPasswordV1::SHA512 { - hash: hash.clone().into(), - }, - Kdf::SSHA512(salt, hash) => ReplPasswordV1::SSHA512 { - salt: salt.clone().into(), - hash: hash.clone().into(), - }, - Kdf::NT_MD4(hash) => ReplPasswordV1::NT_MD4 { - hash: hash.clone().into(), + Kdf::CRYPT_MD5 { s, h } => DbPasswordV1::CRYPT_MD5 { + s: s.clone().into(), + h: h.clone().into(), }, + Kdf::CRYPT_SHA256 { h } => DbPasswordV1::CRYPT_SHA256 { h: h.clone() }, + Kdf::CRYPT_SHA512 { h } => DbPasswordV1::CRYPT_SHA512 { h: h.clone() }, } } @@ -1402,7 +1283,10 @@ impl Password { | Kdf::SSHA256(_, _) | Kdf::SHA512(_) | Kdf::SSHA512(_, _) - | Kdf::NT_MD4(_) => true, + | Kdf::NT_MD4(_) + | Kdf::CRYPT_MD5 { .. } + | Kdf::CRYPT_SHA256 { .. } + | Kdf::CRYPT_SHA512 { .. } => true, } } } @@ -1660,6 +1544,39 @@ mod tests { } } + #[test] + fn test_password_from_crypt_md5() { + sketching::test_init(); + let im_pw = "{crypt}$1$zaRIAsoe$7887GzjDTrst0XbDPpF5m."; + 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_crypt_sha256() { + sketching::test_init(); + let im_pw = "{crypt}$5$3UzV7Sut8EHCUxlN$41V.jtMQmFAOucqI4ImFV43r.bRLjPlN.hyfoCdmGE2"; + 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_crypt_sha512() { + sketching::test_init(); + let im_pw = "{crypt}$6$aXn8azL8DXUyuMvj$9aJJC/KEUwygIpf2MTqjQa.f0MEXNg2cGFc62Fet8XpuDVDedM05CweAlxW6GWxnmHqp14CRf6zU7OQoE/bCu0"; + 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_argon2id_hsm_bind() { sketching::test_init();