use md5::Digest as _;
use crate::{records::dataentry::DataChecksum, util::checksum::crc32};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum LegacyHashFamily {
Crc32,
Md5Bare,
Md5SaltedWithPrefix,
Sha1SaltedWithPrefix,
}
pub(crate) fn legacy_hash_family(version: &crate::version::Version) -> LegacyHashFamily {
if version.at_least(5, 3, 9) {
LegacyHashFamily::Sha1SaltedWithPrefix
} else if version.at_least(4, 2, 2) {
LegacyHashFamily::Md5SaltedWithPrefix
} else if version.at_least(4, 2, 0) {
LegacyHashFamily::Md5Bare
} else {
LegacyHashFamily::Crc32
}
}
#[derive(Clone, Debug)]
pub(crate) enum LegacyStoredHash {
Crc32(u32),
Md5Bare([u8; 16]),
Md5Salted { hash: [u8; 16], salt: [u8; 8] },
Sha1Salted { hash: [u8; 20], salt: [u8; 8] },
}
pub(crate) const PASSWORD_CHECK_HASH_PREFIX: &[u8] = b"PasswordCheckHash";
pub(crate) fn verify_password_legacy(
password: &str,
stored: &LegacyStoredHash,
unicode: bool,
) -> bool {
let pwd: Vec<u8> = if unicode {
crate::crypto::pbkdf2::password_bytes_utf16le(password)
} else {
password.bytes().collect()
};
match stored {
LegacyStoredHash::Crc32(expected) => {
crc32(&pwd) == *expected
}
LegacyStoredHash::Md5Bare(expected) => {
let mut h = md5::Md5::new();
h.update(&pwd);
let actual = h.finalize();
actual.as_slice() == expected.as_slice()
}
LegacyStoredHash::Md5Salted { hash, salt } => {
let mut h = md5::Md5::new();
h.update(PASSWORD_CHECK_HASH_PREFIX);
h.update(salt);
h.update(&pwd);
let actual = h.finalize();
actual.as_slice() == hash.as_slice()
}
LegacyStoredHash::Sha1Salted { hash, salt } => {
let mut h = sha1::Sha1::new();
h.update(PASSWORD_CHECK_HASH_PREFIX);
h.update(salt);
h.update(&pwd);
let actual = h.finalize();
actual.as_slice() == hash.as_slice()
}
}
}
pub(crate) fn arc4_chunk_key(
password: &str,
chunk_salt: &[u8; 8],
use_sha1: bool,
unicode: bool,
) -> Vec<u8> {
let pwd: Vec<u8> = if unicode {
crate::crypto::pbkdf2::password_bytes_utf16le(password)
} else {
password.bytes().collect()
};
if use_sha1 {
let mut h = sha1::Sha1::new();
h.update(chunk_salt);
h.update(&pwd);
h.finalize().to_vec()
} else {
let mut h = md5::Md5::new();
h.update(chunk_salt);
h.update(&pwd);
h.finalize().to_vec()
}
}
#[cfg_attr(not(test), allow(dead_code))]
pub(crate) fn checksum_label(c: &DataChecksum) -> &'static str {
match c {
DataChecksum::Adler32(_) => "Adler32",
DataChecksum::Crc32(_) => "CRC32",
DataChecksum::Md5(_) => "MD5",
DataChecksum::Sha1(_) => "SHA-1",
DataChecksum::Sha256(_) => "SHA-256",
}
}
#[cfg(test)]
mod tests {
use sha1::Digest as _;
use super::*;
fn v(a: u8, b: u8, c: u8) -> crate::version::Version {
crate::version::Version {
a,
b,
c,
d: 0,
flags: crate::version::VersionFlags::UNICODE,
raw_marker: [0u8; 64],
}
}
#[test]
fn family_per_version() {
assert_eq!(legacy_hash_family(&v(4, 1, 0)), LegacyHashFamily::Crc32);
assert_eq!(legacy_hash_family(&v(4, 2, 0)), LegacyHashFamily::Md5Bare);
assert_eq!(
legacy_hash_family(&v(4, 2, 2)),
LegacyHashFamily::Md5SaltedWithPrefix
);
assert_eq!(
legacy_hash_family(&v(5, 3, 9)),
LegacyHashFamily::Sha1SaltedWithPrefix
);
assert_eq!(
legacy_hash_family(&v(6, 0, 0)),
LegacyHashFamily::Sha1SaltedWithPrefix
);
}
#[test]
fn verify_md5_bare_round_trip() {
let pwd_utf16: Vec<u8> = "hunter2"
.encode_utf16()
.flat_map(|u| u.to_le_bytes())
.collect();
let mut h = md5::Md5::new();
h.update(&pwd_utf16);
let digest = h.finalize();
let mut hash = [0u8; 16];
hash.copy_from_slice(&digest);
let stored = LegacyStoredHash::Md5Bare(hash);
assert!(verify_password_legacy("hunter2", &stored, true));
assert!(!verify_password_legacy("wrong", &stored, true));
}
#[test]
fn verify_sha1_salted_round_trip() {
let salt = [0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88];
let pwd_utf16: Vec<u8> = "test123"
.encode_utf16()
.flat_map(|u| u.to_le_bytes())
.collect();
let mut h = sha1::Sha1::new();
h.update(PASSWORD_CHECK_HASH_PREFIX);
h.update(salt);
h.update(&pwd_utf16);
let digest = h.finalize();
let mut hash = [0u8; 20];
hash.copy_from_slice(&digest);
let stored = LegacyStoredHash::Sha1Salted { hash, salt };
assert!(verify_password_legacy("test123", &stored, true));
assert!(!verify_password_legacy("test1234", &stored, true));
}
#[test]
fn verify_md5_salted_round_trip() {
let salt = [0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00, 0x11];
let pwd_utf16: Vec<u8> = "p".encode_utf16().flat_map(|u| u.to_le_bytes()).collect();
let mut h = md5::Md5::new();
h.update(PASSWORD_CHECK_HASH_PREFIX);
h.update(salt);
h.update(&pwd_utf16);
let digest = h.finalize();
let mut hash = [0u8; 16];
hash.copy_from_slice(&digest);
let stored = LegacyStoredHash::Md5Salted { hash, salt };
assert!(verify_password_legacy("p", &stored, true));
assert!(!verify_password_legacy("q", &stored, true));
}
#[test]
fn verify_crc32_pre_4_2() {
let pwd_utf16: Vec<u8> = "x".encode_utf16().flat_map(|u| u.to_le_bytes()).collect();
let stored = LegacyStoredHash::Crc32(crc32(&pwd_utf16));
assert!(verify_password_legacy("x", &stored, true));
assert!(!verify_password_legacy("y", &stored, true));
}
#[test]
fn arc4_key_md5_vs_sha1_differ_in_length_and_bytes() {
let salt = [1, 2, 3, 4, 5, 6, 7, 8];
let md5_key = arc4_chunk_key("test", &salt, false, true);
let sha1_key = arc4_chunk_key("test", &salt, true, true);
assert_eq!(md5_key.len(), 16, "MD5 digest");
assert_eq!(sha1_key.len(), 20, "SHA-1 digest");
assert_ne!(&md5_key[..], &sha1_key[..16]);
}
#[test]
fn arc4_key_changes_with_salt() {
let s1 = [1u8; 8];
let s2 = [2u8; 8];
assert_ne!(
arc4_chunk_key("x", &s1, true, true),
arc4_chunk_key("x", &s2, true, true)
);
}
#[test]
fn arc4_key_ansi_differs_from_unicode() {
let salt = [9u8; 8];
let unicode_key = arc4_chunk_key("test123", &salt, true, true);
let ansi_key = arc4_chunk_key("test123", &salt, true, false);
assert_ne!(unicode_key, ansi_key);
}
}