Initial commit: noxis JVM bytecode obfuscator

Workspace of 9 crates (noxis, noxis-core, noxis-classfile, noxis-analysis,
noxis-passes, noxis-runtime, noxis-jar, noxis-verify, noxis-structure-check).
AGPL-3.0-only license, Rust 2024 edition, unsafe_code denied.
This commit is contained in:
Loki 2026-10-08 22:01:03 +02:00
commit a641e69455
140 changed files with 17197 additions and 0 deletions

View file

@ -0,0 +1,108 @@
//! Кодирование шифротекста в вид, допустимый для `CONSTANT_String`.
//!
//! LDC работает только со строками (modified UTF-8), поэтому байты шифротекста
//! кодируем ASCII-алфавитом: без Base64 из JDK и без лишних зависимостей.
const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
/// Ошибка кодирования.
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum CodecError {
#[error("некорректный base64")]
InvalidBase64,
}
/// Base64 (стандартный алфавит, без переносов строк).
pub fn encode(data: &[u8]) -> String {
let mut out = String::with_capacity(data.len().div_ceil(3) * 4);
for chunk in data.chunks(3) {
let b0 = chunk[0] as u32;
let b1 = *chunk.get(1).unwrap_or(&0) as u32;
let b2 = *chunk.get(2).unwrap_or(&0) as u32;
let triple = (b0 << 16) | (b1 << 8) | b2;
out.push(ALPHABET[(triple >> 18) as usize & 63] as char);
out.push(ALPHABET[(triple >> 12) as usize & 63] as char);
if chunk.len() > 1 {
out.push(ALPHABET[(triple >> 6) as usize & 63] as char);
} else {
out.push('=');
}
if chunk.len() > 2 {
out.push(ALPHABET[triple as usize & 63] as char);
} else {
out.push('=');
}
}
out
}
fn val(c: u8) -> Option<u32> {
match c {
b'A'..=b'Z' => Some(u32::from(c - b'A')),
b'a'..=b'z' => Some(u32::from(c - b'a') + 26),
b'0'..=b'9' => Some(u32::from(c - b'0') + 52),
b'+' => Some(62),
b'/' => Some(63),
_ => None,
}
}
/// Base64 → байты.
pub fn decode(text: &str) -> Result<Vec<u8>, CodecError> {
let bytes = text.as_bytes();
if !bytes.len().is_multiple_of(4) {
return Err(CodecError::InvalidBase64);
}
let mut out = Vec::with_capacity(bytes.len() / 4 * 3);
for chunk in bytes.chunks(4) {
let mut acc = 0u32;
let mut pads = 0;
for (i, &c) in chunk.iter().enumerate() {
if c == b'=' {
if i < 2 {
return Err(CodecError::InvalidBase64);
}
pads += 1;
acc <<= 6;
continue;
}
let v = val(c).ok_or(CodecError::InvalidBase64)?;
acc = (acc << 6) | v;
}
out.push((acc >> 16) as u8);
if pads < 2 {
out.push((acc >> 8) as u8);
}
if pads < 1 {
out.push(acc as u8);
}
}
Ok(out)
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used, clippy::unwrap_used)]
use super::*;
#[test]
fn roundtrip() {
for data in [
&b""[..],
b"a",
b"ab",
b"abc",
b"abcd",
&[0u8, 255, 128, 7][..],
] {
let enc = encode(data);
assert_eq!(decode(&enc).expect("decode"), data, "data={data:?}");
}
}
#[test]
fn rejects_garbage() {
assert!(decode("abc").is_err());
assert!(decode("!!!!").is_err());
}
}

View file

@ -0,0 +1,196 @@
//! KDF декодера: FNV-1a64 поверх `master || domain || salt`.
//!
//! Зеркало `noxis_core::derive_seed` (тот же FNV-1a64, те же домены).
//! Вход собирается `ByteBuffer`, чтобы не материализовывать 26 байт
//! поштучно: `allocate.putLong(master).put(domain).putLong(salt).array()`.
//!
//! Здесь же — строитель тела метода и короткие эмиссии (общие для всех
//! ветвей декодера).
use noxis_classfile::{Arith, BranchCond, Cond, Const, Insn, InvokeKind, Label, Prim, SlotKind};
/// Строитель тела метода: метки, слоты, короткие эмиссии.
pub(super) struct Gen {
pub insns: Vec<Insn>,
labels: u32,
pub slots: u16,
}
impl Gen {
pub(super) fn new() -> Self {
Self {
insns: Vec::new(),
labels: 0,
// Слоты 0 (`cipher`) и 1 (`salt`) — параметры метода.
slots: 2,
}
}
pub(super) fn label(&mut self) -> Label {
let label = self.labels;
self.labels += 1;
label
}
/// Занять слоты (`wide` — пара под `long`).
pub(super) fn alloc(&mut self, wide: bool) -> u16 {
let slot = self.slots;
self.slots += if wide { 2 } else { 1 };
slot
}
pub(super) fn emit(&mut self, insn: Insn) {
self.insns.push(insn);
}
pub(super) fn mark(&mut self, label: Label) {
self.emit(Insn::Mark(label));
}
}
/// Короткие эмиссии операндов (`writer` выбирает `iconst`/`bipush` сам).
pub(super) fn c_int(value: i32) -> Insn {
Insn::Const(Const::Int(value))
}
pub(super) fn c_long(value: i64) -> Insn {
Insn::Const(Const::Long(value))
}
pub(super) fn c_str(value: &str) -> Insn {
Insn::Const(Const::String(value.to_string()))
}
pub(super) fn load(slot: u16, kind: SlotKind) -> Insn {
Insn::Load { slot, kind }
}
pub(super) fn store(slot: u16, kind: SlotKind) -> Insn {
Insn::Store { slot, kind }
}
pub(super) fn call(owner: &str, name: &str, descriptor: &str, kind: InvokeKind) -> Insn {
Insn::Invoke {
kind,
owner: owner.to_string(),
name: name.to_string(),
descriptor: descriptor.to_string(),
itf: false,
}
}
/// FNV-1a64 init (знаковое представление для `ldc2_w`).
const FNV_INIT: i64 = 0xCBF2_9CE4_8422_2325u64 as i64;
/// FNV-1a64 prime.
const FNV_PRIME: i64 = 0x0000_0100_0000_01B3;
/// Вычислить `derive_seed(master, domain, salt)`; вернуть слот пары.
pub(super) fn emit_kdf(asm: &mut Gen, master: u16, domain: &str) -> u16 {
let input = asm.alloc(false);
build_input(asm, master, domain, input);
let hash = asm.alloc(true);
let index = asm.alloc(false);
asm.emit(c_long(FNV_INIT));
asm.emit(store(hash, SlotKind::Long));
asm.emit(c_int(0));
asm.emit(store(index, SlotKind::Int));
let again = asm.label();
let done = asm.label();
asm.mark(again);
asm.emit(load(index, SlotKind::Int));
asm.emit(load(input, SlotKind::Ref));
asm.emit(Insn::ArrayLength);
asm.emit(Insn::Branch(BranchCond::Int(Cond::Ge), done));
asm.emit(load(hash, SlotKind::Long));
asm.emit(load(input, SlotKind::Ref));
asm.emit(load(index, SlotKind::Int));
asm.emit(Insn::ArrayLoad(noxis_classfile::ArrayKind::Byte));
asm.emit(Insn::Convert(noxis_classfile::Convert::I2L));
asm.emit(c_long(0xFF));
asm.emit(Insn::Binary {
prim: Prim::Long,
op: Arith::And,
});
asm.emit(Insn::Binary {
prim: Prim::Long,
op: Arith::Xor,
});
asm.emit(store(hash, SlotKind::Long));
asm.emit(load(hash, SlotKind::Long));
asm.emit(c_long(FNV_PRIME));
asm.emit(Insn::Binary {
prim: Prim::Long,
op: Arith::Mul,
});
asm.emit(store(hash, SlotKind::Long));
asm.emit(Insn::Iinc { index, delta: 1 });
asm.emit(Insn::Goto(again));
asm.mark(done);
let out = asm.alloc(true);
asm.emit(load(hash, SlotKind::Long));
asm.emit(store(out, SlotKind::Long));
out
}
/// Собрать вход `master || domain || salt` в слот `input`.
///
/// Rust считает FNV по little-endian байтам, а `putLong` кладёт big-endian —
/// поэтому оба `long` разворачиваются (`Long.reverseBytes`).
fn build_input(asm: &mut Gen, master: u16, domain: &str, input: u16) {
asm.emit(c_int(16 + domain.len() as i32));
asm.emit(call(
"java/nio/ByteBuffer",
"allocate",
"(I)Ljava/nio/ByteBuffer;",
InvokeKind::Static,
));
asm.emit(Insn::Dup);
asm.emit(load(master, SlotKind::Long));
asm.emit(call(
"java/lang/Long",
"reverseBytes",
"(J)J",
InvokeKind::Static,
));
asm.emit(call(
"java/nio/ByteBuffer",
"putLong",
"(J)Ljava/nio/ByteBuffer;",
InvokeKind::Virtual,
));
asm.emit(Insn::Dup);
asm.emit(c_str(domain));
asm.emit(call(
"java/lang/String",
"getBytes",
"()[B",
InvokeKind::Virtual,
));
asm.emit(call(
"java/nio/ByteBuffer",
"put",
"([B)Ljava/nio/ByteBuffer;",
InvokeKind::Virtual,
));
asm.emit(load(1, SlotKind::Int));
asm.emit(Insn::Convert(noxis_classfile::Convert::I2L));
asm.emit(call(
"java/lang/Long",
"reverseBytes",
"(J)J",
InvokeKind::Static,
));
asm.emit(call(
"java/nio/ByteBuffer",
"putLong",
"(J)Ljava/nio/ByteBuffer;",
InvokeKind::Virtual,
));
asm.emit(call(
"java/nio/ByteBuffer",
"array",
"()[B",
InvokeKind::Virtual,
));
asm.emit(store(input, SlotKind::Ref));
}

View file

@ -0,0 +1,186 @@
//! Рантайм-декодер: ABI, который получает байткод после подстановки строк.
//!
//! ABI v1 (стабильный, его же нужно повторить на Java side):
//!
//! ```text
//! static String d(String cipher, int salt)
//! ```
//!
//! - `cipher` — base64 шифротекста (схема выбирается по salt: `scheme = all[salt % n]`);
//! - `salt` — индекс строки, по нему детерминированно выводятся key и nonce;
//! - мастер-секрет **не передаётся на вызове** и не лежит в одной константе:
//! он собирается из двух частей (`k1 ^ k2`) прямо в декодере.
//!
//! Класс версии 49: циклы верифицируются выводом типов без `StackMapTable`
//! (проверено загрузкой v49 на Java 21), кадры не генерируются.
mod kdf;
mod xor;
mod xtea;
use noxis_classfile::{
Arith, BranchCond, ClassBuilder, ClassFile, Code, Cond, Insn, InvokeKind, Prim, SlotKind,
Version,
};
use crate::scheme::Scheme;
use kdf::{Gen, c_int, c_long, c_str, call, load, store};
/// Имя метода декодера.
pub const DECODER_METHOD: &str = "d";
/// Дескриптор метода декодера.
pub const DECODER_DESC: &str = "(Ljava/lang/String;I)Ljava/lang/String;";
/// Ошибка генерации рантайма.
#[derive(Debug, thiserror::Error)]
pub enum RuntimeError {
#[error("не реализовано: {0}")]
Unimplemented(&'static str),
#[error("нет активных схем шифрования")]
NoSchemes,
}
/// Сгенерировать класс-декодер (мастер-секрет + расшифровка всех схем).
pub fn decoder_class(
internal_name: &str,
master: u64,
schemes: &[&str],
) -> Result<ClassFile, RuntimeError> {
if schemes.is_empty() {
return Err(RuntimeError::NoSchemes);
}
let mut parsed = Vec::with_capacity(schemes.len());
for name in schemes {
parsed.push(Scheme::parse(name).map_err(|_| RuntimeError::NoSchemes)?);
}
let mut asm = Gen::new();
let master_slot = emit_master(&mut asm, master);
let key = kdf::emit_kdf(&mut asm, master_slot, "string/key");
let nonce = kdf::emit_kdf(&mut asm, master_slot, "string/nonce");
let data = emit_base64(&mut asm);
let len = emit_len(&mut asm, data);
emit_dispatch(&mut asm, &parsed, key, nonce, data, len);
emit_epilogue(&mut asm, data);
let code = Code {
// Точный пик — 10 слотов (замер 08.10.2026); запас без вреда:
// лимит обязан лишь покрывать фактическую глубину.
max_stack: 12,
max_locals: asm.slots,
insns: asm.insns,
exception_table: Vec::new(),
frames: Vec::new(),
attributes: Vec::new(),
};
Ok(ClassBuilder::new(internal_name.to_string(), None)
.version(Version {
minor: 0,
major: 49,
})
.method_with_code(0x0009, DECODER_METHOD, DECODER_DESC, code)
.build())
}
/// Имя класса-декодера для прогона: детерминированно от сида, но выглядит
/// как обычный обфусцированный класс.
pub fn decoder_owner(seed_name: &str) -> String {
seed_name.to_string()
}
/// Пролог: `master = k1 ^ k2`; возвращает слот пары. Ни одна часть не равна
/// мастер-секрету (вторая — `master ^ k1`, первая — потоковая).
fn emit_master(asm: &mut Gen, master: u64) -> u16 {
use noxis_core::Rng;
let mut rng = Rng::from_seed(master ^ 0x9E37_79B9_7F4A_7C15);
let first = rng.next_u64() as i64;
let second = (master ^ first as u64) as i64;
let slot = asm.alloc(true);
asm.emit(c_long(first));
asm.emit(c_long(second));
asm.emit(Insn::Binary {
prim: Prim::Long,
op: Arith::Xor,
});
asm.emit(store(slot, SlotKind::Long));
slot
}
/// `data = Base64.getDecoder().decode(cipher)`; возвращает слот массива.
fn emit_base64(asm: &mut Gen) -> u16 {
let slot = asm.alloc(false);
asm.emit(call(
"java/util/Base64",
"getDecoder",
"()Ljava/util/Base64$Decoder;",
InvokeKind::Static,
));
asm.emit(load(0, SlotKind::Ref));
asm.emit(call(
"java/util/Base64$Decoder",
"decode",
"(Ljava/lang/String;)[B",
InvokeKind::Virtual,
));
asm.emit(store(slot, SlotKind::Ref));
slot
}
/// `len = data.length`; возвращает слот.
fn emit_len(asm: &mut Gen, data: u16) -> u16 {
let slot = asm.alloc(false);
asm.emit(load(data, SlotKind::Ref));
asm.emit(Insn::ArrayLength);
asm.emit(store(slot, SlotKind::Int));
slot
}
/// Диспетчер схем: `salt % n` лестницей `if_icmpne` (n == 1 — без диспетчера).
fn emit_dispatch(asm: &mut Gen, schemes: &[Scheme], key: u16, nonce: u16, data: u16, len: u16) {
if schemes.len() == 1 {
dispatch_branch(asm, schemes[0], key, nonce, data, len);
return;
}
let done = asm.label();
for (index, scheme) in schemes.iter().enumerate() {
if index + 1 == schemes.len() {
break;
}
let next = asm.label();
asm.emit(load(1, SlotKind::Int));
asm.emit(c_int(schemes.len() as i32));
asm.emit(Insn::Binary {
prim: Prim::Int,
op: Arith::Rem,
});
asm.emit(c_int(index as i32));
asm.emit(Insn::Branch(BranchCond::Int(Cond::Ne), next));
dispatch_branch(asm, *scheme, key, nonce, data, len);
asm.emit(Insn::Goto(done));
asm.mark(next);
}
dispatch_branch(asm, schemes[schemes.len() - 1], key, nonce, data, len);
asm.mark(done);
}
fn dispatch_branch(asm: &mut Gen, scheme: Scheme, key: u16, nonce: u16, data: u16, len: u16) {
match scheme {
Scheme::XorSplitmix => xor::emit_xor(asm, key, nonce, data, len),
Scheme::XteaCtr => xtea::emit_xtea(asm, key, nonce, data, len),
}
}
/// Эпилог: `return new String(data, "UTF-8")`.
fn emit_epilogue(asm: &mut Gen, data: u16) {
asm.emit(Insn::New("java/lang/String".to_string()));
asm.emit(Insn::Dup);
asm.emit(load(data, SlotKind::Ref));
asm.emit(c_str("UTF-8"));
asm.emit(call(
"java/lang/String",
"<init>",
"([BLjava/lang/String;)V",
InvokeKind::Special,
));
asm.emit(Insn::AReturn);
}

View file

@ -0,0 +1,243 @@
//! Ветвь `xor-splitmix`: keystream из xoshiro256++ поверх SplitMix-сида.
//!
//! Зеркало `scheme::xor_splitmix` + `Rng::{from_seed, next_u64}` побитово:
//! сид `key ^ rotl(nonce, 17)`, стрим LE-байтами `next_u64`.
use noxis_classfile::{Arith, ArrayKind, BranchCond, Cond, Convert, Insn, Prim, SlotKind};
use super::kdf::{Gen, c_int, c_long, load, store};
/// Расшифровать `data` на месте; `len` — длина массива.
pub(super) fn emit_xor(asm: &mut Gen, key: u16, nonce: u16, data: u16, len: u16) {
let liga = Slots::alloc(asm);
emit_seed(asm, key, nonce, &liga);
emit_states(asm, &liga);
emit_loop(asm, data, len, &liga);
}
/// Слоты ветви (выделяются один раз, используются последовательно).
struct Slots {
seed: u16,
state: [u16; 4],
stream: u16,
index: u16,
inner: u16,
byte: u16,
tmp_a: u16,
tmp_b: u16,
tmp_c: u16,
}
impl Slots {
fn alloc(asm: &mut Gen) -> Self {
Self {
seed: asm.alloc(true),
state: [
asm.alloc(true),
asm.alloc(true),
asm.alloc(true),
asm.alloc(true),
],
stream: asm.alloc(true),
index: asm.alloc(false),
inner: asm.alloc(false),
byte: asm.alloc(false),
tmp_a: asm.alloc(true),
tmp_b: asm.alloc(true),
tmp_c: asm.alloc(true),
}
}
}
/// `seed = key ^ rotl(nonce, 17)`.
fn emit_seed(asm: &mut Gen, key: u16, nonce: u16, slots: &Slots) {
asm.emit(load(nonce, SlotKind::Long));
asm.emit(store(slots.tmp_a, SlotKind::Long));
asm.emit(load(slots.tmp_a, SlotKind::Long));
asm.emit(c_int(17));
asm.emit(shift(Arith::Shl));
asm.emit(store(slots.tmp_b, SlotKind::Long));
asm.emit(load(slots.tmp_a, SlotKind::Long));
asm.emit(c_int(47));
asm.emit(shift(Arith::Ushr));
asm.emit(load(slots.tmp_b, SlotKind::Long));
asm.emit(long_op(Arith::Or));
asm.emit(load(key, SlotKind::Long));
asm.emit(long_op(Arith::Xor));
asm.emit(store(slots.seed, SlotKind::Long));
}
/// `s[i] = splitmix(seed)` четыре раза.
fn emit_states(asm: &mut Gen, slots: &Slots) {
asm.emit(load(slots.seed, SlotKind::Long));
asm.emit(store(slots.tmp_a, SlotKind::Long));
for state in slots.state {
splitmix(asm, slots.tmp_a, state);
}
}
/// Один `splitmix64`: `sm += PHI`, финализатор — в `out`.
fn splitmix(asm: &mut Gen, sm: u16, out: u16) {
const PHI: i64 = 0x9E37_79B9_7F4A_7C15u64 as i64;
const C1: i64 = 0xBF58_476D_1CE4_E5B9u64 as i64;
const C2: i64 = 0x94D0_49BB_1331_11EBu64 as i64;
asm.emit(load(sm, SlotKind::Long));
asm.emit(c_long(PHI));
asm.emit(long_op(Arith::Add));
asm.emit(Insn::Dup2);
asm.emit(store(sm, SlotKind::Long));
finalize(asm, 30, C1);
finalize(asm, 27, C2);
asm.emit(Insn::Dup2);
asm.emit(c_int(31));
asm.emit(shift(Arith::Ushr));
asm.emit(long_op(Arith::Xor));
asm.emit(store(out, SlotKind::Long));
}
/// `(z ^ (z >>> amount)) * c` для вершины стека (результат остаётся наверху).
fn finalize(asm: &mut Gen, amount: i32, constant: i64) {
asm.emit(Insn::Dup2);
asm.emit(c_int(amount));
asm.emit(shift(Arith::Ushr));
asm.emit(long_op(Arith::Xor));
asm.emit(c_long(constant));
asm.emit(long_op(Arith::Mul));
}
/// Цикл по чанкам: свежий `next_u64` на 8 байт, внутренний — по байтам.
///
/// Без `skip`-джойна: стрим всегда свеж на входе во внутренний цикл.
fn emit_loop(asm: &mut Gen, data: u16, len: u16, slots: &Slots) {
asm.emit(c_int(0));
asm.emit(store(slots.index, SlotKind::Int));
let outer = asm.label();
let outer_done = asm.label();
asm.mark(outer);
asm.emit(load(slots.index, SlotKind::Int));
asm.emit(load(len, SlotKind::Int));
asm.emit(Insn::Branch(BranchCond::Int(Cond::Ge), outer_done));
emit_next(asm, slots);
asm.emit(c_int(0));
asm.emit(store(slots.inner, SlotKind::Int));
let inner = asm.label();
let inner_done = asm.label();
asm.mark(inner);
asm.emit(load(slots.inner, SlotKind::Int));
asm.emit(c_int(8));
asm.emit(Insn::Branch(BranchCond::Int(Cond::Ge), inner_done));
asm.emit(load(slots.index, SlotKind::Int));
asm.emit(load(slots.inner, SlotKind::Int));
asm.emit(int_op(Arith::Add));
asm.emit(load(len, SlotKind::Int));
asm.emit(Insn::Branch(BranchCond::Int(Cond::Ge), inner_done));
asm.emit(load(slots.stream, SlotKind::Long));
asm.emit(load(slots.inner, SlotKind::Int));
asm.emit(c_int(3));
asm.emit(int_op(Arith::Shl));
asm.emit(shift(Arith::Ushr));
asm.emit(Insn::Convert(Convert::L2I));
asm.emit(c_int(0xFF));
asm.emit(int_op(Arith::And));
asm.emit(store(slots.byte, SlotKind::Int));
asm.emit(load(data, SlotKind::Ref));
asm.emit(load(slots.index, SlotKind::Int));
asm.emit(load(slots.inner, SlotKind::Int));
asm.emit(int_op(Arith::Add));
asm.emit(Insn::Dup2);
asm.emit(Insn::ArrayLoad(ArrayKind::Byte));
asm.emit(load(slots.byte, SlotKind::Int));
asm.emit(int_op(Arith::Xor));
asm.emit(Insn::ArrayStore(ArrayKind::Byte));
asm.emit(Insn::Iinc {
index: slots.inner,
delta: 1,
});
asm.emit(Insn::Goto(inner));
asm.mark(inner_done);
asm.emit(load(slots.index, SlotKind::Int));
asm.emit(c_int(8));
asm.emit(int_op(Arith::Add));
asm.emit(store(slots.index, SlotKind::Int));
asm.emit(Insn::Goto(outer));
asm.mark(outer_done);
}
/// Один `next_u64` xoshiro256++ в `stream` (точное зеркало `Rng`).
fn emit_next(asm: &mut Gen, slots: &Slots) {
let s = slots.state;
// result = rotl(s0 + s3, 23) + s0
asm.emit(load(s[0], SlotKind::Long));
asm.emit(load(s[3], SlotKind::Long));
asm.emit(long_op(Arith::Add));
asm.emit(store(slots.tmp_a, SlotKind::Long));
asm.emit(load(slots.tmp_a, SlotKind::Long));
asm.emit(c_int(23));
asm.emit(shift(Arith::Shl));
asm.emit(store(slots.tmp_b, SlotKind::Long));
asm.emit(load(slots.tmp_a, SlotKind::Long));
asm.emit(c_int(41));
asm.emit(shift(Arith::Ushr));
asm.emit(load(slots.tmp_b, SlotKind::Long));
asm.emit(long_op(Arith::Or));
asm.emit(load(s[0], SlotKind::Long));
asm.emit(long_op(Arith::Add));
asm.emit(store(slots.stream, SlotKind::Long));
// t = s1 << 17
asm.emit(load(s[1], SlotKind::Long));
asm.emit(c_int(17));
asm.emit(shift(Arith::Shl));
asm.emit(store(slots.tmp_c, SlotKind::Long));
// s2 ^= s0; s3 ^= s1; s1 ^= s2; s0 ^= s3 — строго по порядку.
xor_into(asm, s[2], s[0]);
xor_into(asm, s[3], s[1]);
xor_into(asm, s[1], s[2]);
xor_into(asm, s[0], s[3]);
// s2 ^= t
xor_into(asm, s[2], slots.tmp_c);
// s3 = rotl(s3, 45)
asm.emit(load(s[3], SlotKind::Long));
asm.emit(store(slots.tmp_a, SlotKind::Long));
asm.emit(load(slots.tmp_a, SlotKind::Long));
asm.emit(c_int(45));
asm.emit(shift(Arith::Shl));
asm.emit(store(slots.tmp_b, SlotKind::Long));
asm.emit(load(slots.tmp_a, SlotKind::Long));
asm.emit(c_int(19));
asm.emit(shift(Arith::Ushr));
asm.emit(load(slots.tmp_b, SlotKind::Long));
asm.emit(long_op(Arith::Or));
asm.emit(store(s[3], SlotKind::Long));
}
/// `dst ^= src` для long-слотов.
fn xor_into(asm: &mut Gen, dst: u16, src: u16) {
asm.emit(load(dst, SlotKind::Long));
asm.emit(load(src, SlotKind::Long));
asm.emit(long_op(Arith::Xor));
asm.emit(store(dst, SlotKind::Long));
}
/// `ladd/lsub/...` для `long` (операнды уже на стеке).
fn long_op(op: Arith) -> Insn {
Insn::Binary {
prim: Prim::Long,
op,
}
}
/// Сдвиг `long` на `int` (`lshl`/`lushr`, операнды уже на стеке).
fn shift(op: Arith) -> Insn {
Insn::Binary {
prim: Prim::Long,
op,
}
}
/// `iadd/isub/...` для `int` (операнды уже на стеке).
fn int_op(op: Arith) -> Insn {
Insn::Binary {
prim: Prim::Int,
op,
}
}

View file

@ -0,0 +1,245 @@
//! Ветвь `xtea-ctr`: XTEA в режиме счётчика, побайтовый XOR на месте.
//!
//! Зеркало `scheme::xtea_ctr` побитово: ключ — четыре `u32` из key/nonce,
//! счётчик — номер чанка, все правые сдвиги — логические (`u32` в Rust).
use noxis_classfile::{Arith, ArrayKind, BranchCond, Cond, Convert, Insn, Prim, SlotKind};
use super::kdf::{Gen, c_int, load, store};
/// XTEA delta — знаковое представление для `ldc_w`.
const DELTA: i32 = 0x9E37_79B9u32 as i32;
/// Расшифровать `data` на месте; `len` — длина массива.
pub(super) fn emit_xtea(asm: &mut Gen, key: u16, nonce: u16, data: u16, len: u16) {
let slots = Slots::alloc(asm);
emit_key_array(asm, key, nonce, &slots);
asm.emit(c_int(0));
asm.emit(store(slots.chunk, SlotKind::Int));
let outer = asm.label();
let done = asm.label();
asm.mark(outer);
asm.emit(load(slots.chunk, SlotKind::Int));
asm.emit(load(len, SlotKind::Int));
asm.emit(Insn::Branch(BranchCond::Int(Cond::Ge), done));
emit_counter(asm, &slots);
emit_rounds(asm, &slots);
emit_xor_bytes(asm, data, len, &slots);
asm.emit(load(slots.chunk, SlotKind::Int));
asm.emit(c_int(8));
asm.emit(int_op(Arith::Add));
asm.emit(store(slots.chunk, SlotKind::Int));
asm.emit(Insn::Goto(outer));
asm.mark(done);
}
/// Слоты ветви.
struct Slots {
keys: u16,
chunk: u16,
value0: u16,
value1: u16,
sum: u16,
round: u16,
counter: u16,
tmp_a: u16,
tmp_b: u16,
src: u16,
shift: u16,
sub: u16,
index: u16,
}
impl Slots {
fn alloc(asm: &mut Gen) -> Self {
Self {
keys: asm.alloc(false),
chunk: asm.alloc(false),
value0: asm.alloc(false),
value1: asm.alloc(false),
sum: asm.alloc(false),
round: asm.alloc(false),
counter: asm.alloc(false),
tmp_a: asm.alloc(false),
tmp_b: asm.alloc(false),
src: asm.alloc(false),
shift: asm.alloc(false),
sub: asm.alloc(false),
index: asm.alloc(false),
}
}
}
/// `k = [key.lo, key.hi, nonce.lo, nonce.hi]` как `int[4]`.
fn emit_key_array(asm: &mut Gen, key: u16, nonce: u16, slots: &Slots) {
asm.emit(c_int(4));
asm.emit(Insn::NewArray(ArrayKind::Int));
asm.emit(store(slots.keys, SlotKind::Ref));
for (position, (source, high)) in [(key, false), (key, true), (nonce, false), (nonce, true)]
.into_iter()
.enumerate()
{
asm.emit(load(slots.keys, SlotKind::Ref));
asm.emit(c_int(position as i32));
asm.emit(load(source, SlotKind::Long));
if high {
asm.emit(c_int(32));
asm.emit(long_shift(Arith::Ushr));
asm.emit(Insn::Convert(Convert::L2I));
} else {
asm.emit(Insn::Convert(Convert::L2I));
}
asm.emit(Insn::ArrayStore(ArrayKind::Int));
}
}
/// Счётчик чанка: `v0 = 0`, `v1 = chunk`, `sum = round = 0`.
fn emit_counter(asm: &mut Gen, slots: &Slots) {
asm.emit(load(slots.chunk, SlotKind::Int));
asm.emit(c_int(3));
asm.emit(int_op(Arith::Ushr));
asm.emit(store(slots.counter, SlotKind::Int));
asm.emit(c_int(0));
asm.emit(store(slots.value0, SlotKind::Int));
asm.emit(load(slots.counter, SlotKind::Int));
asm.emit(store(slots.value1, SlotKind::Int));
asm.emit(c_int(0));
asm.emit(store(slots.sum, SlotKind::Int));
asm.emit(c_int(0));
asm.emit(store(slots.round, SlotKind::Int));
}
/// 32 раунда XTEA над `(v0, v1, sum)`.
fn emit_rounds(asm: &mut Gen, slots: &Slots) {
let again = asm.label();
let done = asm.label();
asm.mark(again);
asm.emit(load(slots.round, SlotKind::Int));
asm.emit(c_int(32));
asm.emit(Insn::Branch(BranchCond::Int(Cond::Ge), done));
mixing(asm, slots.value0, slots.value1, 0, slots);
asm.emit(load(slots.sum, SlotKind::Int));
asm.emit(c_int(DELTA));
asm.emit(int_op(Arith::Add));
asm.emit(store(slots.sum, SlotKind::Int));
mixing(asm, slots.value1, slots.value0, 11, slots);
asm.emit(Insn::Iinc {
index: slots.round,
delta: 1,
});
asm.emit(Insn::Goto(again));
asm.mark(done);
}
/// Половина раунда: `dst += (((src<<4)^(src>>>5)) + src) ^ (sum + k[(sum>>>s)&3])`.
fn mixing(asm: &mut Gen, dst: u16, src: u16, key_shift: i32, slots: &Slots) {
asm.emit(load(src, SlotKind::Int));
asm.emit(c_int(4));
asm.emit(int_op(Arith::Shl));
asm.emit(store(slots.tmp_a, SlotKind::Int));
asm.emit(load(src, SlotKind::Int));
asm.emit(c_int(5));
asm.emit(int_op(Arith::Ushr));
asm.emit(store(slots.tmp_b, SlotKind::Int));
asm.emit(load(slots.tmp_a, SlotKind::Int));
asm.emit(load(slots.tmp_b, SlotKind::Int));
asm.emit(int_op(Arith::Xor));
asm.emit(load(src, SlotKind::Int));
asm.emit(int_op(Arith::Add));
asm.emit(store(slots.tmp_a, SlotKind::Int));
asm.emit(load(slots.sum, SlotKind::Int));
asm.emit(load(slots.keys, SlotKind::Ref));
asm.emit(load(slots.sum, SlotKind::Int));
// Первая половина: `sum & 3` без сдвига; вторая: `(sum >>> s) & 3`.
if key_shift != 0 {
asm.emit(c_int(key_shift));
asm.emit(int_op(Arith::Ushr));
}
asm.emit(c_int(3));
asm.emit(int_op(Arith::And));
asm.emit(Insn::ArrayLoad(ArrayKind::Int));
asm.emit(int_op(Arith::Add));
asm.emit(store(slots.tmp_b, SlotKind::Int));
asm.emit(load(slots.tmp_a, SlotKind::Int));
asm.emit(load(slots.tmp_b, SlotKind::Int));
asm.emit(int_op(Arith::Xor));
asm.emit(load(dst, SlotKind::Int));
asm.emit(int_op(Arith::Add));
asm.emit(store(dst, SlotKind::Int));
}
/// XOR 8 байт чанка со стримом `(v0, v1)`; хвост короче 8 — частично.
fn emit_xor_bytes(asm: &mut Gen, data: u16, len: u16, slots: &Slots) {
asm.emit(c_int(0));
asm.emit(store(slots.index, SlotKind::Int));
let again = asm.label();
let done = asm.label();
let use_v0 = asm.label();
let have_src = asm.label();
asm.mark(again);
asm.emit(load(slots.index, SlotKind::Int));
asm.emit(c_int(8));
asm.emit(Insn::Branch(BranchCond::Int(Cond::Ge), done));
asm.emit(load(len, SlotKind::Int));
asm.emit(load(slots.chunk, SlotKind::Int));
asm.emit(load(slots.index, SlotKind::Int));
asm.emit(int_op(Arith::Add));
asm.emit(Insn::Branch(BranchCond::Int(Cond::Le), done));
asm.emit(load(slots.index, SlotKind::Int));
asm.emit(c_int(4));
asm.emit(int_op(Arith::And));
asm.emit(Insn::Branch(BranchCond::Zero(Cond::Eq), use_v0));
asm.emit(load(slots.value1, SlotKind::Int));
asm.emit(Insn::Goto(have_src));
asm.mark(use_v0);
asm.emit(load(slots.value0, SlotKind::Int));
asm.mark(have_src);
asm.emit(store(slots.src, SlotKind::Int));
asm.emit(load(slots.index, SlotKind::Int));
asm.emit(c_int(3));
asm.emit(int_op(Arith::And));
asm.emit(store(slots.tmp_a, SlotKind::Int));
asm.emit(c_int(3));
asm.emit(load(slots.tmp_a, SlotKind::Int));
asm.emit(int_op(Arith::Sub));
asm.emit(c_int(3));
asm.emit(int_op(Arith::Shl));
asm.emit(store(slots.shift, SlotKind::Int));
asm.emit(load(slots.src, SlotKind::Int));
asm.emit(load(slots.shift, SlotKind::Int));
asm.emit(int_op(Arith::Ushr));
asm.emit(c_int(0xFF));
asm.emit(int_op(Arith::And));
asm.emit(store(slots.sub, SlotKind::Int));
asm.emit(load(data, SlotKind::Ref));
asm.emit(load(slots.chunk, SlotKind::Int));
asm.emit(load(slots.index, SlotKind::Int));
asm.emit(int_op(Arith::Add));
asm.emit(Insn::Dup2);
asm.emit(Insn::ArrayLoad(ArrayKind::Byte));
asm.emit(load(slots.sub, SlotKind::Int));
asm.emit(int_op(Arith::Xor));
asm.emit(Insn::ArrayStore(ArrayKind::Byte));
asm.emit(Insn::Iinc {
index: slots.index,
delta: 1,
});
asm.emit(Insn::Goto(again));
asm.mark(done);
}
/// Сдвиг `long` на `int` (`lshl`/`lushr`).
fn long_shift(op: Arith) -> Insn {
Insn::Binary {
prim: Prim::Long,
op,
}
}
/// `iadd/isub/...` для `int`.
fn int_op(op: Arith) -> Insn {
Insn::Binary {
prim: Prim::Int,
op,
}
}

View file

@ -0,0 +1,10 @@
//! # noxis-runtime
//!
//! Схемы шифрования строк и ABI рантайм-декодера, который получает байткод.
pub mod codec;
pub mod decoder;
pub mod scheme;
pub use decoder::{DECODER_DESC, DECODER_METHOD, decoder_class, decoder_owner};
pub use scheme::{Blob, Scheme, decrypt, encrypt};

View file

@ -0,0 +1,203 @@
//! Схемы шифрования строк: пер-строчный ключ, nonce и несколько алгоритмов.
//!
//! Требования (см. `info/evo-weaknesses.md` и `docs/techniques.md`):
//! - **разные схемы между строками** — один скрипт не покрывает весь jar;
//! - **ключ не рядом с шифротекстом** — на вызов передаётся только salt,
//! мастер-секрет живёт в рантайм-декодере (сборка мастер-секрета — P2);
//! - **nonce уникален на строку** — keystream не повторяется;
//! - **MAC** — повреждение/подмена шифротекста ловится, а не молча портит текст.
use noxis_core::{Rng, derive_seed};
use crate::codec;
/// Ошибка схемы.
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum SchemeError {
#[error("неизвестная схема `{0}`")]
UnknownScheme(String),
#[error("повреждённый шифротекст")]
CorruptCiphertext,
}
/// Поддерживаемые схемы.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Scheme {
/// XOR со keystream из xoshiro-like генератора (быстро, дёшево).
XorSplitmix,
/// XTEA в режиме счётчика (блочная, другая структура шифротекста).
XteaCtr,
}
impl Scheme {
/// Имя схемы, как она пишется в конфиге.
pub const fn name(self) -> &'static str {
match self {
Scheme::XorSplitmix => "xor-splitmix",
Scheme::XteaCtr => "xtea-ctr",
}
}
/// Разбор имени из конфига.
pub fn parse(name: &str) -> Result<Self, SchemeError> {
match name {
"xor-splitmix" => Ok(Scheme::XorSplitmix),
"xtea-ctr" => Ok(Scheme::XteaCtr),
other => Err(SchemeError::UnknownScheme(other.to_string())),
}
}
/// Все схемы в порядке объявления (для ротации).
pub const fn all() -> [Scheme; 2] {
[Scheme::XorSplitmix, Scheme::XteaCtr]
}
}
/// Зашифрованный блоб: шифротекст (base64) + tag для целостности.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Blob {
/// base64 шифротекста.
pub cipher: String,
/// 32-битный MAC по ключу от plaintext — ловит подмену/порчу.
pub tag: u32,
}
/// Ключ строки из мастер-секрета и salt (то же вычисление делает Java-декодер).
pub fn string_key(master: u64, salt: i32) -> u64 {
derive_seed(master, "string/key", u64::from(salt as u32))
}
/// Nonce строки: детерминированный от salt, но другой домен — keystream не
/// пересекается даже при совпадении ключей.
pub fn string_nonce(master: u64, salt: i32) -> u64 {
derive_seed(master, "string/nonce", u64::from(salt as u32))
}
/// Зашифровать строку.
pub fn encrypt(scheme: Scheme, master: u64, salt: i32, plain: &str) -> Blob {
let key = string_key(master, salt);
let nonce = string_nonce(master, salt);
let mut data = plain.as_bytes().to_vec();
match scheme {
Scheme::XorSplitmix => xor_splitmix(key, nonce, &mut data),
Scheme::XteaCtr => xtea_ctr(key, nonce, &mut data),
}
Blob {
cipher: codec::encode(&data),
tag: mac(key, &data),
}
}
/// Расшифровать блоб (зеркало того, что делает рантайм-декодер).
pub fn decrypt(scheme: Scheme, master: u64, salt: i32, blob: &Blob) -> Result<String, SchemeError> {
let key = string_key(master, salt);
let nonce = string_nonce(master, salt);
let mut data = codec::decode(&blob.cipher).map_err(|_| SchemeError::CorruptCiphertext)?;
if mac(key, &data) != blob.tag {
return Err(SchemeError::CorruptCiphertext);
}
match scheme {
Scheme::XorSplitmix => xor_splitmix(key, nonce, &mut data),
Scheme::XteaCtr => xtea_ctr(key, nonce, &mut data),
}
String::from_utf8(data).map_err(|_| SchemeError::CorruptCiphertext)
}
/// MAC: fnv1a поверх keystream-домена (дёшево, ловит точечные правки).
fn mac(key: u64, data: &[u8]) -> u32 {
let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
for b in data.iter().chain(key.to_le_bytes().iter()) {
hash ^= u64::from(*b);
hash = hash.wrapping_mul(0x0000_0100_0000_01B3);
}
(hash ^ (hash >> 32)) as u32
}
fn xor_splitmix(key: u64, nonce: u64, data: &mut [u8]) {
let mut rng = Rng::from_seed(key ^ nonce.rotate_left(17));
for chunk in data.chunks_mut(8) {
let stream = rng.next_u64().to_le_bytes();
for (b, s) in chunk.iter_mut().zip(stream.iter()) {
*b ^= s;
}
}
}
/// XTEA в режиме счётчика: keystream = XTEA(k, counter), XOR — на месте.
fn xtea_ctr(key: u64, nonce: u64, data: &mut [u8]) {
let k = [
key as u32,
(key >> 32) as u32,
nonce as u32,
(nonce >> 32) as u32,
];
for (i, chunk) in data.chunks_mut(8).enumerate() {
let stream = xtea_encrypt_block(k, (i as u64).to_be_bytes());
for (b, s) in chunk.iter_mut().zip(stream.iter()) {
*b ^= s;
}
}
}
/// Один блок XTEA (32 раунда, delta = 0x9E3779B9).
fn xtea_encrypt_block(k: [u32; 4], block: [u8; 8]) -> [u8; 8] {
let mut v0 = u32::from_be_bytes([block[0], block[1], block[2], block[3]]);
let mut v1 = u32::from_be_bytes([block[4], block[5], block[6], block[7]]);
let mut sum: u32 = 0;
const DELTA: u32 = 0x9E37_79B9;
for _ in 0..32 {
v0 = v0.wrapping_add(
(((v1 << 4) ^ (v1 >> 5)).wrapping_add(v1)) ^ (sum.wrapping_add(k[(sum & 3) as usize])),
);
sum = sum.wrapping_add(DELTA);
v1 = v1.wrapping_add(
(((v0 << 4) ^ (v0 >> 5)).wrapping_add(v0))
^ (sum.wrapping_add(k[((sum >> 11) & 3) as usize])),
);
}
let mut out = [0u8; 8];
out[..4].copy_from_slice(&v0.to_be_bytes());
out[4..].copy_from_slice(&v1.to_be_bytes());
out
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used, clippy::unwrap_used)]
use super::*;
#[test]
fn roundtrip_all_schemes() {
for scheme in Scheme::all() {
for text in ["", "a", "Hello, мир!", "x".repeat(33).as_str()] {
let blob = encrypt(scheme, 42, 7, text);
let back = decrypt(scheme, 42, 7, &blob).expect("decrypt");
assert_eq!(back, text, "scheme={:?}", scheme);
}
}
}
#[test]
fn different_salt_different_cipher() {
let a = encrypt(Scheme::XorSplitmix, 1, 1, "same");
let b = encrypt(Scheme::XorSplitmix, 1, 2, "same");
assert_ne!(a.cipher, b.cipher);
}
#[test]
fn tampering_is_detected() {
let mut blob = encrypt(Scheme::XteaCtr, 5, 3, "sensitive");
let mut bytes = codec::decode(&blob.cipher).expect("b64");
bytes[0] ^= 0xFF;
blob.cipher = codec::encode(&bytes);
assert_eq!(
decrypt(Scheme::XteaCtr, 5, 3, &blob),
Err(SchemeError::CorruptCiphertext)
);
}
#[test]
fn unknown_scheme_rejected() {
assert!(Scheme::parse("rot13").is_err());
}
}