decryptor_s2 0.1.3

A library and cli for decrpting and encrpting S2 10th Anniversary assets
Documentation
use std::path::Path;

use binrw::{BinRead, BinWrite, binrw, helpers::until_eof};
use compression::prelude::{Action, DecodeExt, EncodeExt, LzssCode, LzssDecoder, LzssEncoder};
use crc32fast::hash;
use simple_eyre::eyre::{Result, eyre};

#[binrw]
#[brw(repr = u32)]
#[repr(u32)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Game {
    Dng = u32::from_le_bytes(*b"rc00"),
    Adk = u32::from_le_bytes(*b"sadk"),
}

#[binrw]
#[brw(little, magic = 0x06091812u32, import(file_name: &str))]
pub struct DecompressedFile {
    pub game: Game,
    #[bw(calc = hash(data))]
    file_crc: u32,
    #[br(assert(hash(&make_key(file_name, &game)) == name_crc))]
    #[bw(calc = hash(&make_key(file_name, game)))]
    name_crc: u32,
    #[bw(calc = data.len().try_into().unwrap())]
    size: u32,
    #[br(parse_with = until_eof, map = |x:Vec<u8>| decompress(&mut encrypt_decrypt(make_key(file_name, &game), &x)))]
    #[br(assert(size as usize == data.len()), assert(file_crc == hash(&data)))]
    #[bw(map = |x| encrypt_decrypt(make_key(file_name, game), &compress(x)).collect::<Vec<u8>>())]
    pub data: Vec<u8>,
}

fn rng(seed: u32) -> minstd::MINSTD0 {
    let mut seed = seed as i32 & 0x7fffffff;
    let ones = seed.count_ones() as i32;
    seed = (5..13 - ones).fold(seed, |seed, i| seed | 1 << (17 + i - 2 * i * (i & 1)));
    seed = (5..ones - 19).fold(seed, |seed, i| seed & !(1 << (17 + i - 2 * i * (i & 1))));
    minstd::MINSTD0::seed(seed)
}

fn make_key(file: &str, game: &Game) -> [u8; 16] {
    let key = match game {
        Game::Adk => *uuid::uuid!("bd8cc2bd-3067-4bf8-b49b-1bf9f6822ef4").as_bytes(),
        Game::Dng => *uuid::uuid!("c95946ca-d9f0-4f0a-a100-aab8cbe8db6b").as_bytes(),
    };
    let file = file.to_ascii_lowercase();
    let mut rng = rng(hash(&encoding_rs::WINDOWS_1252.encode(&file).0));
    match &file[file.len() - 4..] {
        ".s2m" | ".sav" => key,
        _ => key.map(|byte| byte ^ rng.next() as u8),
    }
}

fn decompress(iter: &mut impl Iterator<Item = u8>) -> Vec<u8> {
    let mut mode = 0u32;
    let mut totallen: usize = 0;
    let code_iter = std::iter::from_fn(|| {
        if mode & 0x100 == 0 {
            mode = iter.next()? as u32 | 0xff00;
        }
        if mode & 1 != 0 {
            mode >>= 1;
            totallen += 1;
            return Some(LzssCode::Symbol(iter.next()?));
        }
        let curr = iter.next()? as usize;
        let next = iter.next()? as usize;
        let bufferpos = curr | ((next & 0x30) << 4);
        let len = 3 + (next & 0xf);
        let pos = totallen.wrapping_sub(bufferpos + 16 + 1) & 0x3ff;
        mode >>= 1;
        totallen += len;
        Some(LzssCode::Reference { len, pos })
    });
    let d = &mut LzssDecoder::with_dict(0x400, &[b' '; 0x400]);
    code_iter.decode(d).map(Result::unwrap).collect()
}

fn encrypt_decrypt(key: [u8; 16], data: &[u8]) -> impl Iterator<Item = u8> + '_ {
    let mut random = rng(hash(&key));
    let flavor1: Vec<u8> = (0..(random.next() & 0x7F) + 0x80)
        .map(|_| random.next() as u8)
        .collect();
    let flavor2: Vec<u8> = (0..(random.next() & 0xF) + 0x11)
        .map(|_| random.next() as u8)
        .collect();
    let start = random.next() as usize % data.len();
    let step = (random.next() as usize & 0x1FFF) + 0x2000;
    data.iter()
        .zip(flavor1.into_iter().cycle())
        .map(|(byte1, byte2)| byte1 ^ byte2)
        .enumerate()
        .map(move |(i, val)| {
            if i >= start && (i - start) % step == 0 {
                val ^ flavor2[(key[i % key.len()] as usize ^ i) % flavor2.len()]
            } else {
                val
            }
        })
}

fn compress(u: &[u8]) -> Vec<u8> {
    use LzssCode::*;
    let comparison = |lhs, rhs| match (lhs, rhs) {
        (Reference { len, pos: _ }, Reference { len: rlen, pos: _ }) => (rlen).cmp(&len),
        (Symbol(_), Symbol(_)) => std::cmp::Ordering::Equal,
        (_, Symbol(_)) => std::cmp::Ordering::Greater,
        (Symbol(_), _) => std::cmp::Ordering::Less,
    };
    let e = &mut LzssEncoder::with_dict(comparison, 0x400, 18, 3, 2, &[b' '; 0x400]);
    let iter = u.iter().cloned().encode(e, Action::Finish);
    let mut res = Vec::with_capacity(iter.size_hint().0 * 2);
    let mut op_idx = 0;
    let mut op_code = 1u8;
    let mut currpos: usize = 0;
    iter.map(Result::unwrap).for_each(|code| {
        if op_code == 1 {
            op_idx = res.len();
            res.push(0);
        }
        currpos += match code {
            Symbol(b) => {
                res[op_idx] |= op_code;
                res.push(b);
                1
            }
            Reference { len, pos } => {
                let abspos = currpos.wrapping_sub(pos + 16 + 1) & 0x3ff;
                res.push(abspos as u8);
                res.push((abspos >> 4) as u8 & 0x30 | (len as u8 - 3));
                len
            }
        };
        op_code = op_code.rotate_left(1);
    });
    res
}

pub fn decrypt<P: AsRef<Path>>(path: P) -> Result<Option<DecompressedFile>> {
    let path = path.as_ref();
    let mut reader = binrw::io::BufReader::new(std::fs::File::open(path)?);
    let os_str = path.file_name().ok_or(eyre!("Path {path:?} has no name"))?;
    let file_name = os_str
        .to_str()
        .ok_or(eyre!("{os_str:?} can't be converted to a String"))?;
    match DecompressedFile::read_args(&mut reader, (file_name,)) {
        Ok(res) => Ok(Some(res)),
        Err(e) if !matches!(e, binrw::Error::BadMagic { .. }) => Err(e.into()),
        _ => Ok(None),
    }
}

pub fn write_encrypted<P: AsRef<Path>>(path: P, game: Game, data: Vec<u8>) -> Result<()> {
    let path = path.as_ref();
    let os_str = path.file_name().ok_or(eyre!("Path {path:?} has no name"))?;
    let file_name = os_str
        .to_str()
        .ok_or(eyre!("{os_str:?} can't be converted to a String"))?;
    let mut cursor = std::io::Cursor::new(Vec::new());
    DecompressedFile { game, data }.write_args(&mut cursor, (file_name,))?;
    std::fs::write(path, cursor.into_inner())?;
    Ok(())
}