Add crypt formats for password import ()

Adds crypt md5, sha256 and sha512 allowing import of legacy credentials
from external ldap servers.
This commit is contained in:
Firstyear 2025-02-25 21:09:34 +10:00 committed by GitHub
parent 266dc77536
commit 0e0e8ff844
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 242 additions and 210 deletions

12
Cargo.lock generated
View file

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

View file

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

View file

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

View file

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

View file

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