//! Feistel byte decryptor //! Corresponds to feistel_decrypt_next_byte @ 0x8004d4c1 //! //! Each byte is decrypted through: //! 1. Feistel state update (ROL-based mixing) //! 2. S-Box A lookup on byte_counter (4 nibbles → inv → fwd) //! 3. S-Box B key derivation from rotated 0x8262a2877387e56c //! 4. Combined XOR with rotated 0xc4e52cd2e80e33b7 //! 5. Anti-tamper polynomial hash → output byte const KEY_CONST: u64 = 0x8262a2877387e56c; const MIX_CONST: u64 = 0xc4e52cd2e80e33b7; const FINAL_XOR: u32 = 0x0e80eca9; // ── S-Box table extraction ────────────────────────────────────────── const fn u64s_to_bytes(vals: [u64; 32]) -> [u8; 256] { let mut out = [0u8; 256]; let mut i = 0; while i < 32 { let v = vals[i]; out[i * 8 ] = (v & 0xFF) as u8; out[i * 8 + 1] = ((v >> 8) & 0xFF) as u8; out[i * 8 + 2] = ((v >> 16) & 0xFF) as u8; out[i * 8 + 3] = ((v >> 24) & 0xFF) as u8; out[i * 8 + 4] = ((v >> 32) & 0xFF) as u8; out[i * 8 + 5] = ((v >> 40) & 0xFF) as u8; out[i * 8 + 6] = ((v >> 48) & 0xFF) as u8; out[i * 8 + 7] = ((v >> 56) & 0xFF) as u8; i += 1; } out } // Table A forward (offset 0x00..0xFF, first load in decompilation) static SBOX_A_FWD: [u8; 256] = u64s_to_bytes([ 0x9ed15953c4cca4b6, 0x7aa6141967d5c621, 0xdc9783ea6c0e3fe7, 0x2bd2e6b13728c7b9, 0xc55a41435e40bbeb, 0x4e6039525b008151, 0x1094755c8890642a, 0x8f8d0aae1723707c, 0xfddf042465e5c08a, 0xbf1ecaf3275fd376, 0xf085f9302f46d8b7, 0xc820456d7fff22b4, 0x34e05529f1963d1b, 0xde018c871de9b2f6, 0x6bcf7405ad4ccbfe, 0x79ac151af2d7c19f, 0xe3f466913a07a561, 0xa033fc036818809d, 0x35c2b5d9fb317d9b, 0xf5ede247efa2b3e8, 0x3bba9358be5dc9d4, 0x1f631202849289a3, 0x0678ce82ec1c0dee, 0x11b0a72d6a38db2c, 0x9c6efa624a1669d6, 0x509ac3420f6fab95, 0xf799cd57f8567726, 0xafda4fdda84b0c3c, 0x2ee473b825327244, 0x0b864971e18e3ea9, 0x09bc4d9813bd8ba1, 0xaa7b54d036487e08, ]); // Table A inverse (offset 0x100..0x1FF) static SBOX_A_INV: [u8; 256] = u64s_to_bytes([ 0x38565c35fe1b267e, 0x5f1123e743599f52, 0x0e087f546582a3e6, 0x37c10fc2fb7cbc45, 0x3b16d7d1e1001e24, 0xaff9978a493a86a0, 0x5aeb9068acfdea95, 0xf410426eda04dfa8, 0xb61283dc84755b53, 0xceefff6009c5150a, 0x92eea4fa31b44ff0, 0x48aea157bb622719, 0x702f9e170cc388f6, 0xba4146816a7bb08b, 0x3e5d638da7d5dee3, 0x7d1a07b9b501f122, 0xd01f7844e46d403c, 0xc76461766cbf6b5e, 0x7a96aa4ded8c21be, 0x73f8b703ec6f8e74, 0xcb55982db1d3a966, 0x718f91ddd402add8, 0x673f139a72b89d50, 0x93d920140dd2e539, 0xcfc04e2a79283369, 0xcaa29b2ea5f32505, 0x4afc99c451184bf2, 0x2c941d3658a6e034, 0xcce2f7b30bbde8e9, 0x8777db8085292bc6, 0xd632c989cd474c9c, 0xb206f5c8301cab3d, ]); // Table B forward (second load, overwrites stack) static SBOX_B_FWD: [u8; 256] = u64s_to_bytes([ 0x5e63857d691096e3, 0xf5b29f30c1bce088, 0x6ca3117a3571ff48, 0x9abe3a1273b08ee6, 0xdcdbf2e88a26900f, 0x588d282e554f9ddd, 0x566e221899549b84, 0xcab786d861cb47e7, 0x193677c5f7dead8b, 0x31d4bafd333b9823, 0xe40a08392af36221, 0x5d42721afbe25f0e, 0x75ecbd44ce529ee5, 0xa5ea4e04383ef4d7, 0xc60c166acc1fd540, 0xd6150bae7fd3b951, 0x651e7cfe9703c4f6, 0x00141dee607be979, 0x6409c91ca927f1a6, 0x8f252c9ca0c86d59, 0x6f508c02f8d217af, 0xa71b95a24d68132d, 0xc2bf4a4b5c6b6780, 0x43d057a487c7492f, 0xed5beffacf058134, 0x3c7076660193b5ac, 0xc00d2b5aab24b8e1, 0xdf3fa832bb5391aa, 0x37b3c307d106b1f9, 0x45f082fc89da8392, 0xa19478b4294c20eb, 0x3d7e74b6cd4146d9, ]); // Table B inverse (offset 0x100..0x1FF, second load) static SBOX_B_INV: [u8; 256] = u64s_to_bytes([ 0x10253c0ac9a9ccfa, 0xca0dd5e4675bcf42, 0x4fcb2ca86b979a49, 0x55a3b2991803929e, 0x4b3aea77df22e90b, 0x3b1f83a77f580628, 0xfb204001da0c4182, 0x9d433e95ddb16991, 0xd48d768af82ea600, 0x686ec25609a41d87, 0x441eb3887565cead, 0xcdf17eaae1e6af79, 0x8027987c3108fcb6, 0x3872ded2156c52e2, 0xf7ab32b914e7ff17, 0xbd2b8b3d59a0c463, 0xbc9f816f7196b7d0, 0xbb748f89ebe0d605, 0xdcd178378ec59bc6, 0x53a1dbbf7b84364a, 0x12b470a2c1665e73, 0x1a13ed19c750352d, 0x93547a8557eef29c, 0x8cfe2aa5c0e3625c, 0x3411f094aed9b84c, 0xfd48ec6d265a07ac, 0x47b5ba30d3f53360, 0x5d4586efc824164d, 0x2f02646af4c304d8, 0x9029611bf9d7510f, 0xf346390e1c7dbee8, 0xf64ee5b03f235f21, ]); // ── Feistel state ─────────────────────────────────────────────────── struct FeistelState { state: u32, counter: u32, } impl FeistelState { fn new() -> Self { Self { state: 0, counter: 0 } } /// Advance Feistel state. Verified against disasm @ 0x8004d4c1 State '\0'. /// /// tmp = state ^ counter /// new_state = tmp + (ROL32(counter + state, state & 0x1F) ^ ROL32(tmp, counter & 0x1F)) /// counter += 1 fn advance(&mut self) { let counter = self.counter; let state = self.state; self.counter = counter.wrapping_add(1); let tmp = state ^ counter; let rol_a = counter.wrapping_add(state).rotate_left(state & 0x1F); let rol_b = tmp.rotate_left(counter & 0x1F); self.state = tmp.wrapping_add(rol_a ^ rol_b); } } // ── Single-byte decrypt ───────────────────────────────────────────── /// Decrypt one byte from encrypted payload. /// /// raw_byte: the encrypted input byte /// byte_counter: pre-increment counter (0 for first byte) /// feistel: mutable Feistel state (advanced each call) /// /// Returns the decrypted byte. fn decrypt_byte(raw_byte: u8, byte_counter: u32, feistel: &mut FeistelState) -> u8 { // 1. Advance Feistel state feistel.advance(); let st = feistel.state; // new state after advance // 2. S-Box A lookups on byte_counter (4 byte lanes) let b3 = SBOX_A_FWD[SBOX_A_INV[(byte_counter >> 24) as usize] as usize]; let b2 = SBOX_A_FWD[SBOX_A_INV[((byte_counter >> 16) & 0xFF) as usize] as usize]; let b0 = SBOX_A_FWD[SBOX_A_INV[(byte_counter & 0xFF) as usize] as usize]; let b1 = SBOX_A_FWD[SBOX_A_INV[((byte_counter >> 8) & 0xFF) as usize] as usize]; // 3. S-Box B key byte from rotated KEY_CONST let key_idx = (KEY_CONST.rotate_right(st & 0x1F) >> 56) as u8; let key_byte = SBOX_B_FWD[SBOX_B_INV[key_idx as usize] as usize]; // 4. Combined shift derivation let combined: u64 = (b0 as u64) | ((b1 as u64) << 8) | ((b2 as u64) << 16) | ((b3 as u64) << 24) | 0xacacacac_00000000u64; // Arithmetic right shift (sign bit is 1 due to 0xac... in high bytes) let shift = (((combined as i64) >> (raw_byte as u32 & 0x1f)) & 0x1f) as u32; // 5. Mix: key_byte ^ ROL64(MIX_CONST, shift)[31:0] ^ FINAL_XOR let rotated = MIX_CONST.rotate_left(shift) as u32; let mixed = (key_byte as u32) ^ rotated ^ FINAL_XOR; // 6. Polynomial hash → output byte polynomial_mix(raw_byte as u32, byte_counter, st, mixed) } // ── Polynomial anti-tamper hash ───────────────────────────────────── // Faithfully translated from decompilation. All arithmetic is wrapping u32. fn polynomial_mix(raw: u32, n: u32, st: u32, mix: u32) -> u8 { let w = |a: u32, b: u32| -> u32 { a.wrapping_mul(b) }; let wa = |a: u32, b: u32| -> u32 { a.wrapping_add(b) }; // Derived values let shl12 = n << 12; let nshl12 = !shl12; let g = raw | !st; // uVar16 let h = mix | shl12; // uVar22 (reused) let nn = !n; // uVar31 let j = n ^ nshl12; // uVar25 (reused) let k = (n | shl12) ^ nshl12; // uVar26 (reused) let l = st & !raw; // uVar21 (reused) // Linear factors let f7 = w(j, 0x6b00ec19_u32); let f6 = w(k, 0x7ce3eb82_u32); let f17 = w(raw, 0xb19d3cea_u32); // raw * (-0x4e62c316) as u32 let f23 = wa(wa( w(j, 0x1a82aada_u32), w(nshl12, 0xe6b8ed78_u32)), // nshl12 * (-0x19471288) w(nn, 0x013b9852_u32)); let f15 = wa(f17, f23); let f12 = wa(f15, w(k, 0xe441bcd4_u32)); // k * (-0x1bbe432c) let f11 = wa(w(st, 0x1a2900ca_u32), f12); let f13 = w(g, 0x612fbba4_u32); // f14 == f12 (algebraically identical, verified in report §5) let f10 = wa(wa(wa( w(mix, 0xfec467ae_u32), // mix * (-0x13b9852) f13), f12), w(st, 0x4e62c316_u32)); let f8 = w(nshl12, 0xad1a3c4c_u32); // nshl12 * (-0x52e5c3b4) let f9 = w(nn, 0x181b2865_u32); let f5 = w(st, 0x2bb9fc31_u32); let f4 = w(st, 0xd44603ce_u32); // st * (-0x2bb9fc32) let f3 = w(g, 0x0dc3d04a_u32); let f2 = w(mix, 0x38473458_u32); let f1 = w(mix, 0xaf9da343_u32); // mix * (-0x50625cbd) let fh = w(h, 0x181b2865_u32); // Sum of squares * 0xEA let sq_sum = wa(wa(wa(wa(wa(wa(wa(wa(wa(wa( w(f8, f8), w(f9, f9)), w(raw, raw)), w(f7, f7)), w(f6, f6)), w(f5, f5)), w(f4, f4)), w(f3, f3)), w(f2, f2)), w(f1, f1)), w(fh, fh)); let sq_term = w(sq_sum, 0xea_u32); // Cross terms let cross1 = w(w(f8, w(j, 0x1a82aada_u32)), 2); // iVar8 * j * 0x1a82aada * 2 let cross2 = w(w(f7, f17), 2); // iVar7 * iVar17 * 2 let cross3 = w(wa(w(wa(wa(raw, f8), f7), f9), w(raw, f8)), 0xd4); // ((raw+f8+f7)*f9 + raw*f8)*0xd4 let cross4 = w(w(f6, f15), 2); // iVar6 * iVar15 * 2 let cross5 = w(w(f12, f5), 2); // iVar12 * iVar5 * 2 let cross6 = w(w(f11, f4), 2); // iVar11 * iVar4 * 2 let cross7 = w(w(wa(w(st, 0x4e62c316_u32), f12), f3), 2); // (st*0x4e62c316 + f14) * f3 * 2 let cross8 = w(w(f2, wa(wa(w(st, 0x4e62c316_u32), f12), f13)), 2); // f2*(st*0x4e62c316+f12+f13)*2 let cross9 = w(w(wa(wa(wa(w(st, 0x3439c24c_u32), f11), f13), w(mix, 0x13527870_u32)), f1), 2); let cross10 = w(w(f10, fh), 2); // iVar10 * iVar23 * 2 let cross11 = w(w(l, wa(f10, w(h, 0x013b9852_u32))), 0x98); // l*(iVar10+h*0x13b9852)*0x98 // Quadratic self-term: (l * (-0x50386060) + 0x7c) * l let l_quad = w(wa(w(l, 0xafc79fa0_u32), 0x7c), l); // Final assembly let mut result: u32 = 0x455ae97d_u32; // constant base result = wa(result, w(j, 0xa211f9f5_u32)); // j * (-0x5dee060b) result = wa(result, w(st, 0x84a187c3_u32)); // st * (-0x7b5e783d) result = wa(result, l_quad); result = wa(result, w(mix, 0xa1a9cfef_u32)); // mix * (-0x5e563011) result = wa(result, w(wa(h, nn), 0x5e563111_u32)); // (h + nn) * 0x5e563111 result = wa(result, sq_term); result = wa(result, w(nshl12, 0x336bfe1c_u32)); result = wa(result, w(raw, 0x8fd1523d_u32)); // raw * (-0x702eadc3) result = wa(result, w(k, 0x88700dfa_u32)); // k * (-0x778ff206) result = wa(result, cross1); result = wa(result, cross2); result = wa(result, cross3); result = wa(result, cross4); result = wa(result, w(g, 0x1a7751a2_u32)); result = wa(result, w(l, 0x3d99e8a0_u32)); result = wa(result, cross5); result = wa(result, cross6); result = wa(result, cross7); result = wa(result, cross8); result = wa(result, cross9); result = wa(result, cross10); result = wa(result, cross11); result as u8 // low byte = decrypted output } // ── Public API ────────────────────────────────────────────────────── /// Decrypt 33 bytes from the encrypted payload in `d`. /// /// Returns (key_material[32], control_byte). /// /// `data`: the full decoded d-field bytes (part 1, typically 203 bytes) /// `skip`: number of bytes to skip before the 33-byte encrypted window pub fn feistel_decrypt(data: &[u8], skip: usize) -> ([u8; 32], u8) { let mut feistel = FeistelState::new(); let mut key_material = [0u8; 32]; let mut control = 0u8; for i in 0..33 { let raw = data[skip + i]; let decrypted = decrypt_byte(raw, i as u32, &mut feistel); if i < 32 { key_material[i] = decrypted; } else { control = decrypted; } } (key_material, control) }