mirror of
https://github.com/Momoko-Ayase/Senbei.git
synced 2026-09-19 03:57:59 -04:00
refactor: init
This commit is contained in:
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
@@ -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]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user