wx-cli/src/attachment/decoder/v1_xor.rs

167 lines
5.8 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

//! Legacy single-byte XOR decoder无 magic 头的旧 .dat
//!
//! 算法:用已知图片 magic 反推 XOR key —— `key = file[0] ^ magic[0]`。
//! 然后用同一个 key 校验 `file[i] ^ key == magic[i]`,全部命中才接受这个 key。
//!
//! 优先级(按 magic 长度降序,避免短 magic 假阳性):
//! PNG (4) > GIF (4) > TIF (4) > WEBP (4, RIFF) > JPG (3) > BMP (2, 需额外校验)
//!
//! BMP 只有 2 字节 magic假阳性高额外用 BMP file header 里的
//! `bf_size`offset 2, u32 LE和 `bf_offset`offset 10, u32 LE做合理性校验
//! - `|bf_size - file_size| < 1024`(允许微小 padding 差)
//! - `14 <= bf_offset <= 1078`(最大调色板 256*4 + header 14 = 1038留点余量
use anyhow::{anyhow, Result};
use super::{detect_image_format, DecodedImage};
const PNG: &[u8] = &[0x89, 0x50, 0x4E, 0x47];
const GIF: &[u8] = &[0x47, 0x49, 0x46, 0x38];
const TIF: &[u8] = &[0x49, 0x49, 0x2A, 0x00];
const WEBP_RIFF: &[u8] = &[0x52, 0x49, 0x46, 0x46];
const JPG: &[u8] = &[0xFF, 0xD8, 0xFF];
const BMP: &[u8] = &[0x42, 0x4D];
/// 在 `header` 上尝试一个固定 magic返回 `Some(key)` 当且仅当所有字节都对得上。
fn try_magic(header: &[u8], magic: &[u8]) -> Option<u8> {
if header.len() < magic.len() {
return None;
}
let key = header[0] ^ magic[0];
for i in 1..magic.len() {
if header[i] ^ key != magic[i] {
return None;
}
}
Some(key)
}
/// 探测 XOR key。失败返回 `None`caller 决定是不是错)。
pub fn detect_key(file_bytes: &[u8]) -> Option<u8> {
if file_bytes.len() < 4 {
return None;
}
let header = &file_bytes[..file_bytes.len().min(16)];
// 先试 3+ 字节 magic
for magic in [PNG, GIF, TIF, WEBP_RIFF, JPG] {
if let Some(k) = try_magic(header, magic) {
return Some(k);
}
}
// 最后试 BMP只有 2B magic需额外校验
if let Some(k) = try_magic(header, BMP) {
if header.len() >= 14 {
// 解 BMP file header 14 字节
let mut dec = [0u8; 14];
for i in 0..14 {
dec[i] = header[i] ^ k;
}
let bmp_size = u32::from_le_bytes([dec[2], dec[3], dec[4], dec[5]]);
let bmp_offset = u32::from_le_bytes([dec[10], dec[11], dec[12], dec[13]]);
let file_size = file_bytes.len() as u32;
// 允许 1024 字节 padding 差offset 在合理范围
if file_size.abs_diff(bmp_size) < 1024 && (14..=1078).contains(&bmp_offset) {
return Some(k);
}
}
}
None
}
/// XOR 解码整个 `.dat` 内容。
pub fn decode(file_bytes: &[u8]) -> Result<DecodedImage> {
let key =
detect_key(file_bytes).ok_or_else(|| anyhow!("legacy XOR: 无法识别图片 magickey 探测失败)"))?;
let data: Vec<u8> = file_bytes.iter().map(|b| b ^ key).collect();
let format = detect_image_format(&data);
if format == "bin" {
return Err(anyhow!("legacy XOR: 解出 key=0x{:02x} 但产物 magic 不识别", key));
}
Ok(DecodedImage { data, format, decoder: "legacy_xor" })
}
#[cfg(test)]
mod tests {
use super::*;
/// 把一段 plaintext 用单字节 key XOR 加密,模拟 .dat 文件
fn xor_encrypt(plain: &[u8], key: u8) -> Vec<u8> {
plain.iter().map(|b| b ^ key).collect()
}
#[test]
fn detect_jpg_key() {
let plain = vec![0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46];
let enc = xor_encrypt(&plain, 0x3C);
assert_eq!(detect_key(&enc), Some(0x3C));
}
#[test]
fn detect_png_key() {
let mut plain = vec![0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
plain.extend_from_slice(&[0; 16]);
let enc = xor_encrypt(&plain, 0xA5);
assert_eq!(detect_key(&enc), Some(0xA5));
}
#[test]
fn detect_gif_key() {
let mut plain = b"GIF89a".to_vec();
plain.extend_from_slice(&[0; 16]);
let enc = xor_encrypt(&plain, 0x77);
assert_eq!(detect_key(&enc), Some(0x77));
}
#[test]
fn detect_webp_riff_key() {
let mut plain = b"RIFF\x00\x00\x00\x00WEBP".to_vec();
plain.extend_from_slice(&[0; 8]);
let enc = xor_encrypt(&plain, 0x12);
assert_eq!(detect_key(&enc), Some(0x12));
}
#[test]
fn detect_tif_key() {
let mut plain = vec![0x49, 0x49, 0x2A, 0x00, 0x08, 0x00, 0x00, 0x00];
plain.extend_from_slice(&[0; 16]);
let enc = xor_encrypt(&plain, 0xC3);
assert_eq!(detect_key(&enc), Some(0xC3));
}
#[test]
fn detect_bmp_with_valid_header() {
// BMP 14B header: 'BM' + size(u32 LE) + reserved(2*u16) + offset(u32 LE)
let mut plain = Vec::new();
plain.extend_from_slice(b"BM");
plain.extend_from_slice(&100u32.to_le_bytes()); // file_size = 100
plain.extend_from_slice(&[0; 4]); // reserved
plain.extend_from_slice(&54u32.to_le_bytes()); // pixel data offset = 54
plain.resize(100, 0); // 整个文件 100 字节,匹配 file_size
let enc = xor_encrypt(&plain, 0x55);
assert_eq!(detect_key(&enc), Some(0x55));
}
#[test]
fn reject_random_bytes() {
// 全 0 文件BMP 检测会算出 key = 0x42 ^ 0 = 0x42
// 但解密出的 BMP file_size = 0 vs file_size = 100差距 > 1024 →
// 应该 reject
let bytes = vec![0u8; 100];
assert_eq!(detect_key(&bytes), None);
}
#[test]
fn decode_round_trip_jpg() {
let mut plain = vec![0xFF, 0xD8, 0xFF, 0xE0];
plain.extend_from_slice(b"JFIF padding here");
let enc = xor_encrypt(&plain, 0xAB);
let out = decode(&enc).unwrap();
assert_eq!(out.format, "jpg");
assert_eq!(out.decoder, "legacy_xor");
assert_eq!(out.data, plain);
}
}