use anyhow::{anyhow, bail, Context, Result};
use picky_krb::crypto::CipherSuite;
#[allow(unused_imports)]
use tracing;
pub const PAC_CREDENTIAL_INFO: u32 = 2;
pub const KEY_USAGE_KERB_NON_KERB_SALT: i32 = 16;
#[derive(Clone)]
pub struct UnpacCreds {
nt_hash: [u8; 16],
lm_hash: Option<[u8; 16]>,
pub package: String,
}
impl std::fmt::Debug for UnpacCreds {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("UnpacCreds")
.field("nt_hash", &"***")
.field("lm_hash", &self.lm_hash.map(|_| "***"))
.field("package", &self.package)
.finish()
}
}
impl UnpacCreds {
pub fn new(nt_hash: [u8; 16], lm_hash: Option<[u8; 16]>, package: String) -> Self {
Self {
nt_hash,
lm_hash,
package,
}
}
pub fn nt_hash_bytes(&self) -> &[u8; 16] {
&self.nt_hash
}
pub fn lm_hash_bytes(&self) -> Option<&[u8; 16]> {
self.lm_hash.as_ref()
}
pub fn nt_hex(&self) -> String {
self.nt_hash
.iter()
.fold(String::with_capacity(32), |mut s, b| {
use std::fmt::Write;
write!(s, "{b:02x}").unwrap();
s
})
}
}
pub fn unpac_credential_info(pac_bytes: &[u8], session_key: &[u8]) -> Result<Option<UnpacCreds>> {
if pac_bytes.len() < 8 {
bail!("PAC too short to hold a header ({} bytes)", pac_bytes.len());
}
let c_buffers = u32::from_le_bytes(pac_bytes[0..4].try_into().unwrap()) as usize;
let _version = u32::from_le_bytes(pac_bytes[4..8].try_into().unwrap());
let header_len = c_buffers
.checked_mul(16)
.and_then(|n| n.checked_add(8))
.ok_or_else(|| anyhow!("PAC header c_buffers*16+8 overflow (c_buffers={c_buffers})"))?;
if pac_bytes.len() < header_len {
bail!(
"PAC header claims {} buffers but only {} bytes available",
c_buffers,
pac_bytes.len()
);
}
for i in 0..c_buffers {
let d = 8 + i * 16;
let ul_type = u32::from_le_bytes(pac_bytes[d..d + 4].try_into().unwrap());
if ul_type != PAC_CREDENTIAL_INFO {
continue;
}
let cb = u32::from_le_bytes(pac_bytes[d + 4..d + 8].try_into().unwrap()) as usize;
let off = u64::from_le_bytes(pac_bytes[d + 8..d + 16].try_into().unwrap()) as usize;
let end = off.checked_add(cb).ok_or_else(|| {
anyhow!("PAC_CREDENTIAL_INFO offset+size overflow (off={off}, cb={cb})")
})?;
if end > pac_bytes.len() {
bail!(
"PAC_CREDENTIAL_INFO buffer descriptor points past PAC end ({}+{} > {})",
off,
cb,
pac_bytes.len()
);
}
let body = &pac_bytes[off..end];
return Ok(Some(decrypt_and_parse_credential_info(body, session_key)?));
}
Ok(None)
}
pub fn decrypt_and_parse_credential_info(body: &[u8], session_key: &[u8]) -> Result<UnpacCreds> {
if body.len() < 8 {
bail!(
"PAC_CREDENTIAL_INFO body too short ({} bytes, need ≥ 8 for header)",
body.len()
);
}
let version = u32::from_le_bytes(body[0..4].try_into().unwrap());
let etype = u32::from_le_bytes(body[4..8].try_into().unwrap());
let ct = &body[8..];
if version != 0 {
bail!("PAC_CREDENTIAL_INFO Version {version} unrecognised — MS-PAC §2.6.1 defines only 0");
}
let plain = decrypt_serialized(etype, session_key, ct)
.context("decrypt PAC_CREDENTIAL_INFO.SerializedData (usage 16)")?;
parse_pac_credential_data(&plain)
}
fn decrypt_serialized(etype: u32, key: &[u8], ct: &[u8]) -> Result<Vec<u8>> {
fn guarded<F>(label: &'static str, f: F) -> Result<Vec<u8>>
where
F: FnOnce() -> Result<Vec<u8>> + std::panic::UnwindSafe,
{
std::panic::catch_unwind(f).map_err(|_| {
anyhow!("{label} panicked on malformed input (picky-krb 0.9.6 upstream)")
})?
}
let key_owned = key.to_vec();
let ct_owned = ct.to_vec();
let key_ref: &[u8] = &key_owned;
let ct_ref: &[u8] = &ct_owned;
const AES_MIN: usize = 44;
const RC4_MIN: usize = 40;
match etype {
18 => {
if ct.len() < AES_MIN {
bail!(
"PAC_CREDENTIAL_INFO AES256 ciphertext too short ({} bytes, need >= {AES_MIN} for CTS 2-block confounder+plaintext + HMAC)",
ct.len()
);
}
guarded("AES256 decrypt", move || {
CipherSuite::Aes256CtsHmacSha196
.cipher()
.decrypt(key_ref, KEY_USAGE_KERB_NON_KERB_SALT, ct_ref)
.map_err(|e| anyhow!("AES256 decrypt: {e}"))
})
}
17 => {
if ct.len() < AES_MIN {
bail!(
"PAC_CREDENTIAL_INFO AES128 ciphertext too short ({} bytes, need >= {AES_MIN} for CTS 2-block confounder+plaintext + HMAC)",
ct.len()
);
}
guarded("AES128 decrypt", move || {
CipherSuite::Aes128CtsHmacSha196
.cipher()
.decrypt(key_ref, KEY_USAGE_KERB_NON_KERB_SALT, ct_ref)
.map_err(|e| anyhow!("AES128 decrypt: {e}"))
})
}
23 => {
if ct.len() < RC4_MIN {
bail!(
"PAC_CREDENTIAL_INFO RC4-HMAC ciphertext too short ({} bytes, need >= {RC4_MIN} for confounder + block + HMAC per RFC 4757)",
ct.len()
);
}
guarded("RC4-HMAC decrypt", move || {
crate::rc4::decrypt(key_ref, KEY_USAGE_KERB_NON_KERB_SALT, ct_ref)
.map_err(|e| anyhow!("RC4-HMAC decrypt: {e}"))
})
}
other => bail!("unsupported PAC_CREDENTIAL_INFO etype {other} — expected 17/18/23"),
}
}
fn parse_pac_credential_data(plain: &[u8]) -> Result<UnpacCreds> {
if plain.len() < 24 {
bail!(
"PAC_CREDENTIAL_DATA plaintext too short ({} bytes, need ≥ 24 for NDR headers)",
plain.len()
);
}
let after_common = &plain[16..];
if after_common.len() < 8 {
bail!("PAC_CREDENTIAL_DATA missing outer pointer + count");
}
let _referent = u32::from_le_bytes(after_common[0..4].try_into().unwrap());
let credential_count = u32::from_le_bytes(after_common[4..8].try_into().unwrap()) as usize;
if credential_count == 0 {
bail!("PAC_CREDENTIAL_DATA claims zero SECPKG_SUPPLEMENTAL_CRED entries");
}
if credential_count > 1 {
bail!(
"PAC_CREDENTIAL_DATA carries {credential_count} SECPKG_SUPPLEMENTAL_CRED entries — \
only 1 currently supported; extend parse_pac_credential_data() to iterate"
);
}
let mut cursor = &after_common[8..];
if cursor.len() < 4 {
bail!("PAC_CREDENTIAL_DATA truncated at conformant-array max count");
}
cursor = &cursor[4..];
if cursor.len() < 8 {
bail!("PAC_CREDENTIAL_DATA truncated at PackageName header");
}
let pkg_length = u16::from_le_bytes(cursor[0..2].try_into().unwrap()) as usize;
let _pkg_max = u16::from_le_bytes(cursor[2..4].try_into().unwrap());
let _pkg_ref = u32::from_le_bytes(cursor[4..8].try_into().unwrap());
cursor = &cursor[8..];
if cursor.len() < 8 {
bail!("PAC_CREDENTIAL_DATA truncated at CredentialSize + ptr");
}
let cred_size = u32::from_le_bytes(cursor[0..4].try_into().unwrap()) as usize;
let _cred_ptr = u32::from_le_bytes(cursor[4..8].try_into().unwrap());
cursor = &cursor[8..];
if cursor.len() < 12 {
bail!("PAC_CREDENTIAL_DATA truncated at deferred PackageName content");
}
let pkg_max_count = u32::from_le_bytes(cursor[0..4].try_into().unwrap()) as usize;
let _pkg_offset = u32::from_le_bytes(cursor[4..8].try_into().unwrap());
let pkg_actual = u32::from_le_bytes(cursor[8..12].try_into().unwrap()) as usize;
cursor = &cursor[12..];
let pkg_bytes_len = pkg_actual * 2;
if cursor.len() < pkg_bytes_len {
bail!(
"PAC_CREDENTIAL_DATA truncated in PackageName body (need {pkg_bytes_len}, have {})",
cursor.len()
);
}
#[allow(unknown_lints, clippy::chunks_exact_to_as_chunks)]
let pkg_utf16: Vec<u16> = cursor[..pkg_bytes_len]
.chunks_exact(2)
.map(|c| u16::from_le_bytes([c[0], c[1]]))
.collect();
let package = String::from_utf16(&pkg_utf16).unwrap_or_else(|_| "?".to_string());
cursor = &cursor[pkg_bytes_len..];
let after_pkg_offset = pkg_bytes_len; let pad = (4 - after_pkg_offset % 4) % 4;
if cursor.len() < pad {
bail!("PAC_CREDENTIAL_DATA truncated at PackageName trailing pad");
}
cursor = &cursor[pad..];
if pkg_length != pkg_bytes_len {
tracing::debug!(
pkg_length,
pkg_bytes_len,
pkg_max_count,
"PackageName length/actual mismatch — parsing tolerantly"
);
}
if cursor.len() < 4 {
bail!("PAC_CREDENTIAL_DATA truncated at Credentials max_count");
}
let cred_max_count = u32::from_le_bytes(cursor[0..4].try_into().unwrap()) as usize;
cursor = &cursor[4..];
if cred_max_count != cred_size {
bail!(
"PAC_CREDENTIAL_DATA Credentials max_count {cred_max_count} != CredentialSize {cred_size}"
);
}
if cursor.len() < cred_size {
bail!(
"PAC_CREDENTIAL_DATA truncated in Credentials body (need {cred_size}, have {})",
cursor.len()
);
}
let credentials = &cursor[..cred_size];
if credentials.len() < 40 {
bail!(
"NTLM_SUPPLEMENTAL_CREDENTIAL too short ({} bytes, need 40 for V0)",
credentials.len()
);
}
let ntlm_version = u32::from_le_bytes(credentials[0..4].try_into().unwrap());
let flags = u32::from_le_bytes(credentials[4..8].try_into().unwrap());
if ntlm_version != 0 {
bail!("NTLM_SUPPLEMENTAL_CREDENTIAL Version {ntlm_version} — only V0 supported");
}
let lm_present = (flags & 0x0000_0001) != 0;
let nt_present = (flags & 0x0000_0002) != 0;
let mut lm_hash = [0u8; 16];
lm_hash.copy_from_slice(&credentials[8..24]);
let mut nt_hash = [0u8; 16];
nt_hash.copy_from_slice(&credentials[24..40]);
if !nt_present {
bail!("NTLM_SUPPLEMENTAL_CREDENTIAL Flags bit 1 (NT present) is clear — no NT hash to extract");
}
let lm = if lm_present && lm_hash.iter().any(|&b| b != 0) {
Some(lm_hash)
} else {
None
};
Ok(UnpacCreds::new(nt_hash, lm, package))
}
pub fn try_unpac_from_encrypted_pa_data(
padatas: &[(u32, Vec<u8>)],
session_key: &[u8],
) -> Result<Option<UnpacCreds>> {
for (ty, body) in padatas {
if body.len() < 8 || u32::from_le_bytes(body[0..4].try_into().unwrap()) != 0 {
tracing::trace!(
padata_type = ty,
bytes = body.len(),
"skipping padata (not credential-info shape)"
);
continue;
}
match decrypt_and_parse_credential_info(body, session_key) {
Ok(creds) => {
tracing::debug!(
padata_type = ty,
package = %creds.package,
"PAC_CREDENTIAL_INFO extracted from AS-REP padata"
);
return Ok(Some(creds));
}
Err(e) => {
tracing::trace!(
padata_type = ty,
err = %e,
"padata rejected as PAC_CREDENTIAL_INFO"
);
}
}
}
Ok(None)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_hand_crafted_credential_data_extracts_nt_hash() {
let mut buf = vec![
0x01, 0x10, 0x08, 0x00, 0xcc, 0xcc, 0xcc, 0xcc, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ];
buf.extend_from_slice(&0x0002_0000u32.to_le_bytes()); buf.extend_from_slice(&1u32.to_le_bytes()); buf.extend_from_slice(&1u32.to_le_bytes());
buf.extend_from_slice(&8u16.to_le_bytes());
buf.extend_from_slice(&8u16.to_le_bytes());
buf.extend_from_slice(&0x0002_0004u32.to_le_bytes());
buf.extend_from_slice(&40u32.to_le_bytes());
buf.extend_from_slice(&0x0002_0008u32.to_le_bytes());
buf.extend_from_slice(&4u32.to_le_bytes());
buf.extend_from_slice(&0u32.to_le_bytes());
buf.extend_from_slice(&4u32.to_le_bytes());
for c in "NTLM".encode_utf16() {
buf.extend_from_slice(&c.to_le_bytes());
}
buf.extend_from_slice(&40u32.to_le_bytes());
buf.extend_from_slice(&0u32.to_le_bytes()); buf.extend_from_slice(&0x0000_0002u32.to_le_bytes()); buf.extend_from_slice(&[0u8; 16]); let nt_needle: [u8; 16] = [
0x8a, 0xc4, 0x1d, 0x9e, 0x62, 0xbc, 0xe0, 0x1a, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66,
0x77, 0x88,
];
buf.extend_from_slice(&nt_needle);
let creds =
parse_pac_credential_data(&buf).expect("parse hand-crafted PAC_CREDENTIAL_DATA");
assert_eq!(creds.nt_hash_bytes(), &nt_needle);
assert_eq!(
creds.lm_hash_bytes(),
None,
"LM zeros → None (flag says not present)"
);
assert_eq!(creds.package, "NTLM");
assert_eq!(creds.nt_hex(), "8ac41d9e62bce01a1122334455667788");
}
#[test]
fn missing_credential_info_returns_none() {
let mut buf = vec![];
buf.extend_from_slice(&1u32.to_le_bytes()); buf.extend_from_slice(&0u32.to_le_bytes()); buf.extend_from_slice(&1u32.to_le_bytes()); buf.extend_from_slice(&0u32.to_le_bytes()); buf.extend_from_slice(&24u64.to_le_bytes()); let out = unpac_credential_info(&buf, &[0u8; 32]).expect("no error");
assert!(out.is_none(), "no PAC_CREDENTIAL_INFO → None");
}
}