refactor: init

This commit is contained in:
bfloat16
2026-08-11 14:19:56 +08:00
parent a89900a812
commit e9ead4dc5f
65 changed files with 4028 additions and 7442 deletions
+9
View File
@@ -0,0 +1,9 @@
[package]
name = "senbei-crypto"
version.workspace = true
edition.workspace = true
license.workspace = true
description = "Cryptographic and compression primitives for Senbei"
[dependencies]
thiserror.workspace = true
+119
View File
@@ -0,0 +1,119 @@
// Bytecode interpreter for the custom-decryptor stages. Those stages are tiny
// instruction programs embedded in the decrypted buffer; we compile each
// program down to a Vec<Op> and interpret it.
#[derive(Clone, Copy)]
pub enum Op {
Add(u8),
Sub(u8),
Xor(u8),
Rol(u32),
Ror(u32),
Inc,
Dec,
}
pub fn apply(ops: &[Op], mut x: u8) -> u8 {
for &op in ops {
x = match op {
Op::Add(n) => x.wrapping_add(n),
Op::Sub(n) => x.wrapping_sub(n),
Op::Xor(n) => x ^ n,
Op::Rol(n) => x.rotate_left(n & 7),
Op::Ror(n) => x.rotate_right(n & 7),
Op::Inc => x.wrapping_add(1),
Op::Dec => x.wrapping_sub(1),
};
}
x
}
/// A precomputed 256-entry byte→byte translation table for a fixed op list.
///
/// `apply` is a pure function of a single byte, but the hot decrypt paths run it
/// over multi-megabyte regions. Building the full table once and translating
/// each byte with a single lookup turns an O(region × ops) walk into O(region) —
/// a large constant-factor win on those paths.
pub struct OpsLut {
t: [u8; 256],
}
impl OpsLut {
pub fn new(ops: &[Op]) -> Self {
let mut t = [0u8; 256];
let mut i = 0;
while i < 256 {
t[i] = apply(ops, i as u8);
i += 1;
}
Self { t }
}
/// Translate `d[off .. off + n]` in place through the table.
#[inline]
pub fn map_region(&self, d: &mut [u8], off: usize, n: usize) {
for b in &mut d[off..off + n] {
*b = self.t[*b as usize];
}
}
}
pub fn generate(data: &[u8], offset: u32) -> Option<Vec<Op>> {
// Bounds-checked cursor: a corrupt `data_offset` (bad decrypt_data6 / the
// alignment fallback) must yield `None`, not an out-of-bounds panic — the
// panic path would surface as a misleading `UnpackError::InternalPanic` instead
// of the precise `BytecodeGenerationFailed`, and any future caller without a
// `catch_unwind` wrapper would abort outright.
let mut pos = offset as usize;
let mut next = move || {
let b = data.get(pos).copied()?;
pos += 1;
Some(b)
};
let mut ops = Vec::new();
loop {
match next()? {
4 => ops.push(Op::Add(next()?)),
44 => ops.push(Op::Sub(next()?)),
52 => ops.push(Op::Xor(next()?)),
144 => {} // nop
192 => {
let mb = next()?;
let rm = mb & 7;
let reg = (mb >> 3) & 7;
let mod_ = (mb >> 6) & 3;
if mod_ != 3 || rm != 0 {
return None;
}
let imm = next()? as u32;
match reg {
0 => ops.push(Op::Rol(imm)),
1 => ops.push(Op::Ror(imm)),
_ => {
return None;
}
}
}
254 => {
let mb = next()?;
let rm = mb & 7;
let reg = (mb >> 3) & 7;
let mod_ = (mb >> 6) & 3;
if mod_ != 3 || rm != 0 {
return None;
}
match reg {
0 => ops.push(Op::Inc),
1 => ops.push(Op::Dec),
_ => {
return None;
}
}
}
195 => return Some(ops),
_ => {
return None;
}
}
}
}
+33
View File
@@ -0,0 +1,33 @@
const fn build_table() -> [u32; 256] {
let mut table = [0u32; 256];
let mut i = 0;
while i < 256 {
let mut c = i as u32;
let mut k = 0;
while k < 8 {
c = if c & 1 != 0 {
0xEDB8_8320 ^ (c >> 1)
} else {
c >> 1
};
k += 1;
}
table[i] = c;
i += 1;
}
table
}
const TABLE: [u32; 256] = build_table();
pub fn append(initial: u32, data: &[u8]) -> u32 {
let mut crc = !initial;
for &b in data {
crc = TABLE[((crc ^ b as u32) & 0xFF) as usize] ^ (crc >> 8);
}
!crc
}
pub fn compute(data: &[u8]) -> u32 {
append(0, data)
}
+77
View File
@@ -0,0 +1,77 @@
//! Cryptographic, checksum, compression, and bytecode primitives.
pub mod bytecode;
pub mod crc32;
pub mod primitives;
mod tables;
/// Maximum buffer size accepted by allocation-sensitive transforms.
pub const MAX_IMAGE_SIZE: u64 = 1 << 30;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BufferOperation {
Read,
CopySource,
CopyDestination,
ZeroFill,
}
impl std::fmt::Display for BufferOperation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Self::Read => "read",
Self::CopySource => "copy source",
Self::CopyDestination => "copy destination",
Self::ZeroFill => "zero-fill",
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum Error {
#[error(
"{operation} range out of bounds (offset {offset}, size {size}, buffer length {buffer_len})"
)]
BufferRangeOutOfBounds {
operation: BufferOperation,
offset: usize,
size: usize,
buffer_len: usize,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum DecompressionFailure {
#[error("compressed source size {size} exceeds limit {max}")]
SourceTooLarge { size: u32, max: u64 },
#[error("Huffman code length {bits} is invalid")]
InvalidCodeLength { bits: u8 },
#[error("Huffman tree traversal exceeded 64 levels")]
HuffmanTraversalLimit,
#[error("pending length accumulator overflowed at {pending}")]
PendingLengthOverflow { pending: u32 },
#[error("output step {step} at byte {written} exceeds expected size {expected}")]
OutputOverflow {
written: u32,
step: u32,
expected: u32,
},
#[error("run-fill width {width} reads before output offset 0x{destination:08X}")]
RunFillBeforeOutput { width: u32, destination: u32 },
#[error("run-fill width {width} is unsupported")]
InvalidRunFillWidth { width: u32 },
#[error("back-reference distance {distance} exceeds {written} written bytes")]
InvalidBackReference { distance: u32, written: u32 },
#[error("Huffman symbol consumed no input and produced no output")]
NoProgress,
#[error(
"output size mismatch (wrote {written}/{expected} bytes after consuming {consumed}/{source_size})"
)]
OutputSizeMismatch {
written: u32,
expected: u32,
consumed: u32,
source_size: u32,
},
}
File diff suppressed because it is too large Load Diff
+161
View File
@@ -0,0 +1,161 @@
//! AES inverse tables (inverse S-box + InvMixColumns "Td" T-tables), generated
//! at compile time from GF(2^8) arithmetic rather than embedded as a transcribed
//! blob. These are the standard AES *decryption* tables — not proprietary data —
//! so we derive them. The generated bytes are verified byte-identical to the
//! original hand-transcribed arrays (CRC32-locked in the test at the bottom).
//!
//! Each table is 1024 bytes = 256 u32 little-endian, read by
//! `primitives::aes_round` via `get_u32(&TABLE, x * 4)`. The byte layout matches
//! the original exactly, so `aes_round` is unchanged:
//! SBOX[x] = invsbox(x) broadcast to 4 bytes
//! COLUMMIX1[x] = [0b*s, 0d*s, 09*s, 0e*s], s = invsbox(x) (Td0, this byte order)
//! COLUMMIX2/3/4 = COLUMMIX1's 4-byte group rotated left by 1 / 2 / 3 bytes
//!
//! Generated the same way as the existing `const fn` CRC-table generation in
//! `crc32.rs`.
/// GF(2^8) multiply with the AES reduction polynomial (x^8 + x^4 + x^3 + x + 1).
const fn gf_mul(mut a: u8, mut b: u8) -> u8 {
let mut p: u8 = 0;
let mut i = 0;
while i < 8 {
if b & 1 != 0 {
p ^= a;
}
let hi = a & 0x80;
a <<= 1;
if hi != 0 {
a ^= 0x1B;
}
b >>= 1;
i += 1;
}
p
}
/// The AES inverse S-box, derived from the multiplicative inverse in GF(2^8)
/// followed by inverting the forward S-box's affine transform.
const fn inv_sbox() -> [u8; 256] {
// Multiplicative inverse: inv[a] = b such that a*b == 1 (inv[0] stays 0).
let mut inv = [0u8; 256];
let mut a = 1usize;
while a < 256 {
let mut b = 1usize;
while b < 256 {
if gf_mul(a as u8, b as u8) == 1 {
inv[a] = b as u8;
break;
}
b += 1;
}
a += 1;
}
// Forward S-box: affine transform over the inverse.
let mut sb = [0u8; 256];
let mut i = 0usize;
while i < 256 {
let mut x = inv[i];
let mut s = inv[i];
let mut r = 0;
while r < 4 {
s = s.rotate_left(1);
x ^= s;
r += 1;
}
sb[i] = x ^ 0x63;
i += 1;
}
// Inverse S-box is the inverse permutation of the forward S-box.
let mut isb = [0u8; 256];
let mut i = 0usize;
while i < 256 {
isb[sb[i] as usize] = i as u8;
i += 1;
}
isb
}
/// The five generated tables (each 1024 bytes = 256 u32 LE).
struct AesTables {
cm1: [u8; 1024],
cm2: [u8; 1024],
cm3: [u8; 1024],
cm4: [u8; 1024],
sbox: [u8; 1024],
}
/// Build all five tables in one compile-time pass.
const fn build_tables() -> AesTables {
let isb = inv_sbox();
let mut cm1 = [0u8; 1024];
let mut cm2 = [0u8; 1024];
let mut cm3 = [0u8; 1024];
let mut cm4 = [0u8; 1024];
let mut sbox = [0u8; 1024];
let mut x = 0usize;
while x < 256 {
let s = isb[x];
// SBOX: invsbox(x) broadcast to all four lanes.
let mut j = 0;
while j < 4 {
sbox[x * 4 + j] = s;
j += 1;
}
// COLUMMIX1 lane bytes; CM2/3/4 are byte-rotations of the same four.
let b = [
gf_mul(0x0b, s),
gf_mul(0x0d, s),
gf_mul(0x09, s),
gf_mul(0x0e, s),
];
let mut j = 0;
while j < 4 {
cm1[x * 4 + j] = b[j];
cm2[x * 4 + j] = b[(j + 1) % 4];
cm3[x * 4 + j] = b[(j + 2) % 4];
cm4[x * 4 + j] = b[(j + 3) % 4];
j += 1;
}
x += 1;
}
AesTables {
cm1,
cm2,
cm3,
cm4,
sbox,
}
}
const TABLES: AesTables = build_tables();
pub static COLUMMIX1: [u8; 1024] = TABLES.cm1;
pub static COLUMMIX2: [u8; 1024] = TABLES.cm2;
pub static COLUMMIX3: [u8; 1024] = TABLES.cm3;
pub static COLUMMIX4: [u8; 1024] = TABLES.cm4;
pub static SBOX: [u8; 1024] = TABLES.sbox;
#[cfg(test)]
mod tests {
use super::*;
/// Lock the generated tables to the original hand-transcribed bytes. The
/// CRC32 oracles were computed from the previously-committed `tables.rs`
/// arrays; any drift in the generator (or the GF math) fails here before it
/// can reach the byte-identical corpus goldens.
#[test]
fn generated_tables_match_committed_bytes() {
assert_eq!(COLUMMIX1.len(), 1024);
assert_eq!(crate::crc32::compute(&COLUMMIX1), 0x7e8d_5d5f);
assert_eq!(crate::crc32::compute(&COLUMMIX2), 0xfcc4_acfc);
assert_eq!(crate::crc32::compute(&COLUMMIX3), 0x637a_f0cd);
assert_eq!(crate::crc32::compute(&COLUMMIX4), 0x1e7b_c381);
assert_eq!(crate::crc32::compute(&SBOX), 0x10fd_6dc1);
// Spot-check the first dword of each (matches the original first row).
assert_eq!(&COLUMMIX1[..4], &[0x50, 0xa7, 0xf4, 0x51]);
assert_eq!(&COLUMMIX2[..4], &[0xa7, 0xf4, 0x51, 0x50]);
assert_eq!(&COLUMMIX3[..4], &[0xf4, 0x51, 0x50, 0xa7]);
assert_eq!(&COLUMMIX4[..4], &[0x51, 0x50, 0xa7, 0xf4]);
assert_eq!(&SBOX[..4], &[0x52, 0x52, 0x52, 0x52]);
}
}