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:
commit
a641e69455
140 changed files with 17197 additions and 0 deletions
108
crates/noxis-runtime/src/codec.rs
Normal file
108
crates/noxis-runtime/src/codec.rs
Normal 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());
|
||||
}
|
||||
}
|
||||
196
crates/noxis-runtime/src/decoder/kdf.rs
Normal file
196
crates/noxis-runtime/src/decoder/kdf.rs
Normal 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));
|
||||
}
|
||||
186
crates/noxis-runtime/src/decoder/mod.rs
Normal file
186
crates/noxis-runtime/src/decoder/mod.rs
Normal 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);
|
||||
}
|
||||
243
crates/noxis-runtime/src/decoder/xor.rs
Normal file
243
crates/noxis-runtime/src/decoder/xor.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
245
crates/noxis-runtime/src/decoder/xtea.rs
Normal file
245
crates/noxis-runtime/src/decoder/xtea.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
10
crates/noxis-runtime/src/lib.rs
Normal file
10
crates/noxis-runtime/src/lib.rs
Normal 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};
|
||||
203
crates/noxis-runtime/src/scheme.rs
Normal file
203
crates/noxis-runtime/src/scheme.rs
Normal 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());
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue