pub struct RawBcrypt;
use crate::hashes::{AttackModeType, HashModule, HashPattern, ParsedHash};
impl HashModule for RawBcrypt {
fn name(&self) -> &'static str { "bcrypt" }
fn mode(&self) -> u32 { 3200 }
fn digest_words(&self) -> u32 { 6 }
fn needs_int64(&self) -> bool { false }
fn cpu_verify(&self, password: &str, _salt: &[u8], hash: &[u32]) -> bool {
let raw = bcrypt_hash(password, "", 4);
let mut words = [0u32; 6];
for i in 0..6 {
words[i] = u32::from_be_bytes(raw[i * 4..i * 4 + 4].try_into().unwrap());
}
words == hash[..6]
}
fn shader_source(&self, mode: &AttackModeType) -> &'static str {
match mode {
AttackModeType::BruteForce => include_str!("../bcrypt_crack.wgsl"),
AttackModeType::Mask => include_str!("../bcrypt_mask.wgsl"),
AttackModeType::Wordlist => include_str!("../bcrypt_wordlist.wgsl"),
}
}
fn detect_patterns(&self) -> &[HashPattern] {
&[HashPattern { prefix: Some("$2"), hex_len: None, priority: 80 }]
}
fn parse_hash_string(&self, s: &str) -> Result<ParsedHash, String> {
use std::str::FromStr;
let parts = bcrypt::HashParts::from_str(s)
.map_err(|e| format!("Failed to parse bcrypt hash: {}", e))?;
let salt_str = parts.get_salt();
let _cost = parts.get_cost();
let salt_vec = decode_bcrypt_base64(&salt_str, 16)
.map_err(|e| format!("Failed to decode bcrypt salt: {}", e))?;
let mut salt_arr = [0u8; 16];
salt_arr.copy_from_slice(&salt_vec);
let combined = s.rsplit('$').next().unwrap_or("");
if combined.len() < 22 + 31 {
return Err("Bcrypt hash string too short".to_string());
}
let hash_b64 = &combined[22..]; let hash_vec = decode_bcrypt_base64(hash_b64, 23)
.map_err(|e| format!("Failed to decode bcrypt hash: {}", e))?;
let mut hash_bytes = [0u8; 24];
hash_bytes[..23].copy_from_slice(&hash_vec);
let mut hash_words = [0u32; 8];
for i in 0..6 {
let start = i * 4;
let mut buf = [0u8; 4];
buf.copy_from_slice(&hash_bytes[start..start + 4]);
hash_words[i] = u32::from_be_bytes(buf);
}
Ok(ParsedHash {
hash_words,
extra_words: [0u32; 8],
salt: salt_arr.to_vec(),
digest_words: 6,
})
}
}
pub fn bcrypt_hash(password: &str, salt: &str, cost: u32) -> [u8; 24] {
let salt_bytes = if salt.len() == 16 {
let mut arr = [0u8; 16];
arr.copy_from_slice(salt.as_bytes());
arr
} else if !salt.is_empty() {
if let Ok(decoded) = decode_bcrypt_base64(salt, 16) {
let mut arr = [0u8; 16];
arr.copy_from_slice(&decoded);
arr
} else {
let mut arr = [0u8; 16];
let b = salt.as_bytes();
let n = b.len().min(16);
arr[..n].copy_from_slice(&b[..n]);
arr
}
} else {
[0u8; 16]
};
let pwd_bytes = {
let pwd = password.as_bytes();
let mut v = Vec::with_capacity(pwd.len() + 1);
v.extend_from_slice(pwd);
v.push(0);
if v.len() > 72 {
v.truncate(72);
}
v
};
bcrypt::bcrypt(cost, salt_bytes, &pwd_bytes)
}
fn decode_bcrypt_base64_impl(chars: &[u8], expected_bytes: usize) -> Result<Vec<u8>, String> {
const ALPHABET: &[u8] = b"./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
fn idx(c: u8, alph: &[u8]) -> Result<u32, String> {
alph.iter().position(|&x| x == c).map(|i| i as u32).ok_or_else(|| format!("Invalid base64 char: {}", c as char))
}
let mut out = Vec::with_capacity(expected_bytes);
let mut i = 0;
while out.len() < expected_bytes && i + 4 <= chars.len() {
let c0 = idx(chars[i], ALPHABET)?;
let c1 = idx(chars[i + 1], ALPHABET)?;
let c2 = idx(chars[i + 2], ALPHABET)?;
let c3 = idx(chars[i + 3], ALPHABET)?;
let val = (c0 << 18) | (c1 << 12) | (c2 << 6) | c3;
out.push((val >> 16) as u8);
if out.len() < expected_bytes { out.push((val >> 8) as u8); }
if out.len() < expected_bytes { out.push(val as u8); }
i += 4;
}
if out.len() < expected_bytes && i + 2 <= chars.len() {
let c0 = idx(chars[i], ALPHABET)?;
let c1 = idx(chars[i + 1], ALPHABET)?;
let val = (c0 << 18) | (c1 << 12);
out.push((val >> 16) as u8);
if out.len() < expected_bytes && i + 3 <= chars.len() {
let c2 = idx(chars[i + 2], ALPHABET)?;
let val = (c0 << 18) | (c1 << 12) | (c2 << 6);
out.push((val >> 8) as u8);
}
}
if out.len() != expected_bytes {
return Err(format!("Expected {} bytes from base64, got {}", expected_bytes, out.len()));
}
Ok(out)
}
fn decode_bcrypt_base64(s: &str, expected_bytes: usize) -> Result<Vec<u8>, String> {
decode_bcrypt_base64_impl(s.as_bytes(), expected_bytes)
}
#[allow(dead_code)]
fn encode_bcrypt_base64(data: &[u8]) -> String {
const ALPHABET: &[u8] = b"./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let mut out = String::new();
let mut i = 0;
while i + 3 <= data.len() {
let val = (data[i] as u32) << 16 | (data[i + 1] as u32) << 8 | data[i + 2] as u32;
out.push(ALPHABET[(val >> 18 & 0x3f) as usize] as char);
out.push(ALPHABET[(val >> 12 & 0x3f) as usize] as char);
out.push(ALPHABET[(val >> 6 & 0x3f) as usize] as char);
out.push(ALPHABET[(val & 0x3f) as usize] as char);
i += 3;
}
let rem = data.len() - i;
if rem == 1 {
let val = (data[i] as u32) << 16;
out.push(ALPHABET[(val >> 18 & 0x3f) as usize] as char);
out.push(ALPHABET[(val >> 12 & 0x3f) as usize] as char);
} else if rem == 2 {
let val = (data[i] as u32) << 16 | (data[i + 1] as u32) << 8;
out.push(ALPHABET[(val >> 18 & 0x3f) as usize] as char);
out.push(ALPHABET[(val >> 12 & 0x3f) as usize] as char);
out.push(ALPHABET[(val >> 6 & 0x3f) as usize] as char);
}
out
}
#[test]
fn test_bcrypt_verify() {
let hash = bcrypt::hash("abc", 4).unwrap();
eprintln!("BCRYPT HASH FOR abc: {}", hash);
assert!(hash.starts_with("$2"));
assert!(bcrypt::verify("abc", &hash).unwrap());
assert!(!bcrypt::verify("wrong_password", &hash).unwrap());
}
#[test]
fn test_bcrypt_parse_and_verify() {
let hash_str = "$2b$04$crMTmHIF5jdy0n07vh/3ROpLLyDF6ah99PaO/xuI2dazgYeDlMJ82";
use std::str::FromStr;
let parts = bcrypt::HashParts::from_str(hash_str).unwrap();
let salt_str = parts.get_salt();
let cost = parts.get_cost();
eprintln!("Salt string (22 chars): '{}', cost: {}", salt_str, cost);
let salt_vec = decode_bcrypt_base64(&salt_str, 16).unwrap();
eprintln!("Decoded salt (16 bytes): {:02x?}", salt_vec);
let mut salt_arr = [0u8; 16];
salt_arr.copy_from_slice(&salt_vec);
let raw = bcrypt::bcrypt(cost, salt_arr, b"abc\0");
eprintln!("Raw bcrypt hash (24 bytes): {:02x?}", raw);
let combined = hash_str.rsplit('$').next().unwrap_or("");
let hash_b64 = &combined[22..]; eprintln!("Hash part (31 chars): '{}'", hash_b64);
let hash_bytes = decode_bcrypt_base64(hash_b64, 23).unwrap();
eprintln!("Decoded hash from string (23 bytes): {:02x?}", hash_bytes);
let encoded_hash_bytes = encode_bcrypt_base64(&hash_bytes);
eprintln!("Encoded decoded hash (23→31 chars): '{}'", encoded_hash_bytes);
eprintln!("Expected hash b64 (31 chars): '{}'", hash_b64);
let encoded_raw = encode_bcrypt_base64(&raw[..23]);
eprintln!("Encoded raw[..23] (23→31 chars): '{}'", encoded_raw);
let encoded_truncated = &encoded_raw;
assert_eq!(encoded_truncated, hash_b64, "Encoded hash should match hash part");
let encoded_salt = encode_bcrypt_base64(&salt_arr);
eprintln!("Encoded salt (16→22 chars): '{}'", encoded_salt);
eprintln!("Expected salt (22 chars): '{}'", salt_str);
assert_eq!(encoded_salt, salt_str, "Salt roundtrip should match");
let our_result = bcrypt_hash("abc", &salt_str, cost);
eprintln!("Our bcrypt_hash result (24 bytes): {:02x?}", our_result);
assert_eq!(our_result, raw, "Our bcrypt_hash should match crate's bcrypt");
let default_result = bcrypt_hash("abc", "", 4);
eprintln!("Default salt result: {:02x?}", default_result);
}
#[test]
fn test_bcrypt_raw() {
let salt = [0u8; 16];
let result = bcrypt::bcrypt(4, salt, b"abc");
assert_eq!(result.len(), 24);
let result2 = bcrypt::bcrypt(4, salt, b"abc");
assert_eq!(result, result2);
}
#[test]
fn test_bcrypt_base64_roundtrip() {
let data = [0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0,
0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef,
0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88];
let encoded = encode_bcrypt_base64(&data);
let decoded = decode_bcrypt_base64(&encoded, 24).unwrap();
assert_eq!(data.as_slice(), decoded.as_slice());
}
#[test]
fn test_bcrypt_debug_target_words() {
let hash_str = "$2b$04$crMTmHIF5jdy0n07vh/3ROpLLyDF6ah99PaO/xuI2dazgYeDlMJ82";
use std::str::FromStr;
let parts = bcrypt::HashParts::from_str(hash_str).unwrap();
let salt_str = parts.get_salt();
let cost = parts.get_cost();
let salt_vec = decode_bcrypt_base64(&salt_str, 16).unwrap();
let mut salt_arr = [0u8; 16];
salt_arr.copy_from_slice(&salt_vec);
let raw = bcrypt::bcrypt(cost, salt_arr, b"abc\0");
eprintln!("Full 24-byte hash: {:02x?}", raw);
let combined = hash_str.rsplit('$').next().unwrap_or("");
let hash_b64 = &combined[22..];
let hash_bytes_decoded = decode_bcrypt_base64(hash_b64, 23).unwrap();
let mut target_hash_bytes = [0u8; 24];
target_hash_bytes[..23].copy_from_slice(&hash_bytes_decoded);
let mut target_words = [0u32; 6];
for i in 0..6 {
let start = i * 4;
let mut buf = [0u8; 4];
buf.copy_from_slice(&target_hash_bytes[start..start+4]);
target_words[i] = u32::from_be_bytes(buf);
}
let mut gpu_words = [0u32; 6];
for i in 0..6 {
gpu_words[i] = u32::from_be_bytes(raw[i*4..i*4+4].try_into().unwrap());
}
eprintln!("Target words (23-byte): {:08x?}", target_words);
eprintln!("GPU words (24-byte): {:08x?}", gpu_words);
let masked_gpu5 = gpu_words[5] & 0xFFFFFF00;
eprintln!("Masked gpu[5] ({:08x} & FFFFFF00 = {:08x}) vs target[5] ({:08x})",
gpu_words[5], masked_gpu5, target_words[5]);
eprintln!("All 6 match (masked): {}", target_words == gpu_words);
eprintln!("First 5 match: {}", target_words[..5] == gpu_words[..5]);
eprintln!("Word 5 match masked: {}", masked_gpu5 == target_words[5]);
}