use crate::dir_entry::DirEntryName;
use crate::error::{Ext4Error, IncompatibleKind};
use core::mem;
use core::num::Wrapping;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum HashAlg {
HalfMd4,
Tea,
}
impl HashAlg {
pub(crate) fn from_u8(alg: u8) -> Result<Self, Ext4Error> {
if alg == 1 {
Ok(Self::HalfMd4)
} else if alg == 2 {
Ok(Self::Tea)
} else {
Err(IncompatibleKind::DirectoryHash(alg).into())
}
}
pub(crate) fn hash(
&self,
name: DirEntryName<'_>,
mut seed: &[u32; 4],
) -> u32 {
if seed == &[0; 4] {
seed = &[0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476];
}
let mut state = StateBlock::default();
for i in 0..4 {
state[i] = Wrapping(seed[i]);
}
let hash = match self {
Self::HalfMd4 => {
for chunk in
name.as_ref().chunks(mem::size_of::<HashBlock<8>>())
{
let inp = create_hash_block(chunk);
md4_half(&mut state, &inp);
}
state[1].0
}
Self::Tea => {
for chunk in
name.as_ref().chunks(mem::size_of::<HashBlock<4>>())
{
let inp = create_hash_block(chunk);
tea(&mut state, &inp);
}
state[0].0
}
};
hash & !1
}
}
type Wu32 = Wrapping<u32>;
type StateBlock = [Wu32; 4];
type HashBlock<const N: usize> = [Wu32; N];
fn md4_half(state: &mut StateBlock, data: &HashBlock<8>) {
const K1: Wu32 = Wrapping(0x5a82_7999);
const K2: Wu32 = Wrapping(0x6ed9_eba1);
fn f(x: Wu32, y: Wu32, z: Wu32) -> Wu32 {
z ^ (x & (y ^ z))
}
fn g(x: Wu32, y: Wu32, z: Wu32) -> Wu32 {
(x & y) | (x & z) | (y & z)
}
fn h(x: Wu32, y: Wu32, z: Wu32) -> Wu32 {
x ^ y ^ z
}
fn op<F>(f: F, a: Wu32, b: Wu32, c: Wu32, d: Wu32, k: Wu32, s: u32) -> Wu32
where
F: Fn(Wu32, Wu32, Wu32) -> Wu32,
{
let t = a + f(b, c, d) + k;
Wrapping(t.0.rotate_left(s))
}
let mut a = state[0];
let mut b = state[1];
let mut c = state[2];
let mut d = state[3];
for i in [0usize, 4] {
a = op(f, a, b, c, d, data[i], 3);
d = op(f, d, a, b, c, data[i.checked_add(1).unwrap()], 7);
c = op(f, c, d, a, b, data[i.checked_add(2).unwrap()], 11);
b = op(f, b, c, d, a, data[i.checked_add(3).unwrap()], 19);
}
for &i in &[1usize, 0] {
a = op(g, a, b, c, d, data[i] + K1, 3);
d = op(g, d, a, b, c, data[i.checked_add(2).unwrap()] + K1, 5);
c = op(g, c, d, a, b, data[i.checked_add(4).unwrap()] + K1, 9);
b = op(g, b, c, d, a, data[i.checked_add(6).unwrap()] + K1, 13);
}
for &i in &[2usize, 0] {
a = op(h, a, b, c, d, data[i.checked_add(1).unwrap()] + K2, 3);
d = op(h, d, a, b, c, data[i.checked_add(5).unwrap()] + K2, 9);
c = op(h, c, d, a, b, data[i] + K2, 11);
b = op(h, b, c, d, a, data[i.checked_add(4).unwrap()] + K2, 15);
}
state[0] += a;
state[1] += b;
state[2] += c;
state[3] += d;
}
fn tea(state: &mut StateBlock, data: &HashBlock<4>) {
let mut sum: Wu32 = Wrapping(0);
let mut v0: Wu32 = state[0];
let mut v1: Wu32 = state[1];
for _ in 0..16 {
sum += 0x9e3779b9;
v0 += ((v1 << 4) + data[0]) ^ (v1 + sum) ^ ((v1 >> 5) + data[1]);
v1 += ((v0 << 4) + data[2]) ^ (v0 + sum) ^ ((v0 >> 5) + data[3]);
}
state[0] += v0;
state[1] += v1;
}
#[allow(clippy::as_conversions)]
fn sign_extend_byte_to_u32(byte: u8) -> u32 {
let sbyte = byte as i8;
sbyte as u32
}
fn create_hash_block<const N: usize>(mut src: &[u8]) -> HashBlock<N> {
let mut dst = [Wu32::default(); N];
let pad = u32::from_le_bytes([src.len().to_le_bytes()[0]; 4]);
for dst in dst.iter_mut() {
let mut elem = pad;
for _ in 0..4 {
if let Some(src_byte) = src.first() {
let src_u32 = sign_extend_byte_to_u32(*src_byte);
elem = src_u32.wrapping_add(elem << 8);
src = &src[1..];
}
}
*dst = Wrapping(elem);
}
dst
}
#[cfg(test)]
mod tests {
use super::*;
use core::str;
const SEED0: &str = "00000000-0000-0000-0000-000000000000";
const SEED1: &str = "333fa1eb-588c-456e-b81c-d1d343cd0e01";
const SEED2: &str = "0fc48be0-17dc-4791-b120-39964e159a31";
const NON_ASCII_NAME: &str = "NetLock_Arany_=Class_Gold=_FÅ‘tanúsÃtvány.pem";
const MAX_LEN_NAME: &str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTU";
#[test]
fn test_hash_alg_from_u8() {
assert_eq!(HashAlg::from_u8(1).unwrap(), HashAlg::HalfMd4);
assert_eq!(
HashAlg::from_u8(123).unwrap_err(),
IncompatibleKind::DirectoryHash(123)
);
}
#[track_caller]
fn check_hash_block(src: &[u8], expected: [u32; 8]) {
assert_eq!(
create_hash_block::<8>(src)
.iter()
.map(|n| n.0)
.collect::<Vec<_>>(),
expected
);
}
#[test]
fn test_create_hash_block_full() {
let src = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
assert_eq!(src.len(), 52);
#[rustfmt::skip]
let expected = [
u32::from_be_bytes(*b"abcd"),
u32::from_be_bytes(*b"efgh"),
u32::from_be_bytes(*b"ijkl"),
u32::from_be_bytes(*b"mnop"),
u32::from_be_bytes(*b"qrst"),
u32::from_be_bytes(*b"uvwx"),
u32::from_be_bytes(*b"yzAB"),
u32::from_be_bytes(*b"CDEF"),
];
check_hash_block(src, expected);
}
#[test]
fn test_create_hash_block_padding() {
let src = b"abcdefghijklmnopqr";
assert_eq!(src.len(), 0x12);
#[rustfmt::skip]
let expected = [
u32::from_be_bytes(*b"abcd"),
u32::from_be_bytes(*b"efgh"),
u32::from_be_bytes(*b"ijkl"),
u32::from_be_bytes(*b"mnop"),
0x1212_7172,
0x1212_1212,
0x1212_1212,
0x1212_1212,
];
check_hash_block(src, expected);
}
fn seed_from_uuid(uuid: &str) -> [u32; 4] {
assert_eq!(uuid.len(), 36);
let uuid = uuid.replace('-', "");
let bytes: Vec<u32> = uuid
.as_bytes()
.chunks(8)
.map(|chunk| {
u32::from_str_radix(str::from_utf8(chunk).unwrap(), 16)
.unwrap()
.swap_bytes()
})
.collect();
bytes.try_into().unwrap()
}
#[test]
fn test_seed_from_uuid() {
assert_eq!(
seed_from_uuid("333fa1eb-588c-456e-b81c-d1d343cd0e01"),
[0xeba13f33, 0x6e458c58, 0xd3d11cb8, 0x010ecd43]
);
}
#[test]
fn test_dir_hash_md4() {
let name = DirEntryName::try_from(b"abc").unwrap();
assert_eq!(
HashAlg::HalfMd4.hash(name, &seed_from_uuid(SEED1)),
0x25783134
);
assert_eq!(
HashAlg::HalfMd4.hash(name, &seed_from_uuid(SEED2)),
0x4599f742
);
assert_eq!(
HashAlg::HalfMd4.hash(name, &seed_from_uuid(SEED0)),
0xd196a868
);
let name = DirEntryName::try_from(NON_ASCII_NAME).unwrap();
assert_eq!(
HashAlg::HalfMd4.hash(name, &seed_from_uuid(SEED1)),
0xb40a2038
);
assert_eq!(MAX_LEN_NAME.len(), 255);
let name = DirEntryName::try_from(MAX_LEN_NAME).unwrap();
assert_eq!(
HashAlg::HalfMd4.hash(name, &seed_from_uuid(SEED1)),
0xe40e82e0
);
}
#[test]
fn test_dir_hash_tea() {
let name = DirEntryName::try_from(b"abc").unwrap();
assert_eq!(HashAlg::Tea.hash(name, &seed_from_uuid(SEED1)), 0x8abf7e2e);
assert_eq!(HashAlg::Tea.hash(name, &seed_from_uuid(SEED2)), 0xa19eddb4);
assert_eq!(HashAlg::Tea.hash(name, &seed_from_uuid(SEED0)), 0xb1435ec4);
let name = DirEntryName::try_from(NON_ASCII_NAME).unwrap();
assert_eq!(HashAlg::Tea.hash(name, &seed_from_uuid(SEED1)), 0x23d61dc4);
assert_eq!(MAX_LEN_NAME.len(), 255);
let name = DirEntryName::try_from(MAX_LEN_NAME).unwrap();
assert_eq!(HashAlg::Tea.hash(name, &seed_from_uuid(SEED1)), 0xfd79bbac);
}
#[cfg(all(feature = "std", unix))]
fn check_random_names(hash_alg: HashAlg) {
use std::ffi::OsStr;
use std::fs::File;
use std::io::Read;
use std::os::unix::ffi::OsStrExt;
use std::process::Command;
const TOTAL_ITERATIONS: usize = 5000;
fn read_random_bytes(len: usize) -> Vec<u8> {
let mut f = File::open("/dev/urandom").unwrap();
let mut bytes = vec![0; len];
f.read_exact(&mut bytes).unwrap();
bytes
}
fn gen_random_seed() -> String {
let mut s: String = read_random_bytes(16)
.iter()
.map(|b| format!("{b:02x}"))
.collect();
for i in [8, 13, 18, 23] {
s.insert(i, '-');
}
s
}
fn gen_data_to_hash() -> Vec<u8> {
let len = usize::from(read_random_bytes(1)[0]).max(1);
let mut to_hash = read_random_bytes(len);
let bad_chars = [0, b'/', b'"', b'-'];
for b in &mut to_hash {
if bad_chars.contains(b) {
*b = b'a';
}
}
to_hash
}
fn debugfs_cmd(
hash_alg: HashAlg,
to_hash: &[u8],
seed: &str,
) -> Command {
let hash_alg = match hash_alg {
HashAlg::HalfMd4 => "half_md4",
HashAlg::Tea => "tea",
};
let mut req = b"dx_hash -s ".to_vec();
req.extend(seed.as_bytes());
req.extend(format!(" -h {hash_alg:?} \"").as_bytes());
req.extend(to_hash);
req.push(b'"');
let mut cmd = Command::new("debugfs");
cmd.arg("-R").arg(OsStr::from_bytes(&req));
cmd
}
fn get_expected_hash(
hash_alg: HashAlg,
to_hash: &[u8],
seed: &str,
) -> u32 {
let output = debugfs_cmd(hash_alg, to_hash, seed).output().unwrap();
assert!(output.status.success());
let stdout = &output.stdout;
let mut prefix = b"Hash of ".to_vec();
prefix.extend(to_hash);
prefix.extend(b" is 0x");
let rest = &stdout[prefix.len()..];
let space_index = rest.iter().position(|b| *b == b' ').unwrap();
let hash = &rest[..space_index];
let hash = core::str::from_utf8(hash).unwrap();
u32::from_str_radix(hash, 16).unwrap()
}
for _ in 0..TOTAL_ITERATIONS {
let to_hash = gen_data_to_hash();
let seed = gen_random_seed();
let expected_hash = get_expected_hash(hash_alg, &to_hash, &seed);
let actual_hash = hash_alg.hash(
DirEntryName::try_from(to_hash.as_slice()).unwrap(),
&seed_from_uuid(&seed),
);
if actual_hash != expected_hash {
println!("actual_hash={actual_hash:#08x}");
println!("expected_hash={expected_hash:#08x}");
println!("seed={seed}");
println!("to_hash={to_hash:02x?}");
panic!("actual_hash != expected_hash");
}
}
}
#[cfg(all(feature = "std", unix))]
#[test]
#[ignore]
fn test_random_names_half_md4() {
check_random_names(HashAlg::HalfMd4);
}
#[cfg(all(feature = "std", unix))]
#[test]
#[ignore]
fn test_random_names_tea() {
check_random_names(HashAlg::Tea);
}
}