mirror of
https://git.um-react.app/um/lib_um_crypto_rust.git
synced 2026-03-08 04:29:54 +00:00
[xmly] feat #5: implement xmly decipher
This commit is contained in:
@@ -22,3 +22,14 @@ where
|
||||
{
|
||||
ENGINE.decode(data)
|
||||
}
|
||||
|
||||
pub fn decode_overwrite<T>(data: &mut T) -> Result<&[u8], DecodeError>
|
||||
where
|
||||
T: AsMut<[u8]> + ?Sized,
|
||||
{
|
||||
let data = data.as_mut();
|
||||
let decoded = decode(&mut data[..])?;
|
||||
let len = decoded.len();
|
||||
data[..len].copy_from_slice(&decoded);
|
||||
Ok(&data[..len])
|
||||
}
|
||||
|
||||
14
um_crypto/xmly/Cargo.toml
Normal file
14
um_crypto/xmly/Cargo.toml
Normal file
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "umc_xmly"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
aes = "0.8.4"
|
||||
byteorder = "1.5.0"
|
||||
cbc = "0.1.2"
|
||||
cipher = "0.4.4"
|
||||
hex = "0.4.3"
|
||||
lazy_static = "1.5.0"
|
||||
thiserror = "1.0.63"
|
||||
umc_utils = { path = "../utils" }
|
||||
2
um_crypto/xmly/src/__fixture__/.gitignore
vendored
Normal file
2
um_crypto/xmly/src/__fixture__/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/sample.xm
|
||||
*.m4a
|
||||
BIN
um_crypto/xmly/src/__fixture__/x2m_hdr.bin
Normal file
BIN
um_crypto/xmly/src/__fixture__/x2m_hdr.bin
Normal file
Binary file not shown.
BIN
um_crypto/xmly/src/__fixture__/x2m_hdr_plain.bin
Normal file
BIN
um_crypto/xmly/src/__fixture__/x2m_hdr_plain.bin
Normal file
Binary file not shown.
BIN
um_crypto/xmly/src/__fixture__/x3m_hdr.bin
Normal file
BIN
um_crypto/xmly/src/__fixture__/x3m_hdr.bin
Normal file
Binary file not shown.
BIN
um_crypto/xmly/src/__fixture__/x3m_hdr_plain.bin
Normal file
BIN
um_crypto/xmly/src/__fixture__/x3m_hdr_plain.bin
Normal file
Binary file not shown.
75
um_crypto/xmly/src/android.rs
Normal file
75
um_crypto/xmly/src/android.rs
Normal file
@@ -0,0 +1,75 @@
|
||||
use lazy_static::lazy_static;
|
||||
|
||||
pub fn derive_table<const N: usize>(init: f64, step: f64) -> [usize; N] {
|
||||
debug_assert!(step > 0.0);
|
||||
debug_assert!(init > 0.0);
|
||||
|
||||
let mut result = [0usize; N];
|
||||
|
||||
let mut temp = init;
|
||||
let mut data = [0f64; N];
|
||||
for datum in data.iter_mut() {
|
||||
*datum = temp;
|
||||
temp = temp * step * (1.0 - temp);
|
||||
}
|
||||
|
||||
let mut sorted = data;
|
||||
sorted.sort_unstable_by(|a, b| a.total_cmp(b));
|
||||
|
||||
for (item, needle) in result.iter_mut().zip(data) {
|
||||
let idx = sorted
|
||||
.iter()
|
||||
.position(|&x| x == needle)
|
||||
.expect("could not find item");
|
||||
*item = idx;
|
||||
sorted[idx] = -f64::NAN; // values can not be negative, so...
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref TABLE_X2M: [usize; 0x400] = derive_table(0.615243, 3.837465);
|
||||
static ref TABLE_X3M: [usize; 0x400] = derive_table(0.726354, 3.948576);
|
||||
}
|
||||
|
||||
pub enum FileType {
|
||||
X2M,
|
||||
X3M,
|
||||
}
|
||||
|
||||
/// Decrypt the first 0x400 bytes.
|
||||
pub fn decrypt_android(version: FileType, buffer: &mut [u8; 0x400]) {
|
||||
let (content_key, scramble_table) = match version {
|
||||
FileType::X2M => (*b"xmlyxmlyxmlyxmlyxmlyxmlyxmlyxmly", &*TABLE_X2M),
|
||||
FileType::X3M => (*b"3989d111aad5613940f4fc44b639b292", &*TABLE_X3M),
|
||||
};
|
||||
|
||||
let src = *buffer;
|
||||
for (i, hdr) in buffer.iter_mut().enumerate() {
|
||||
*hdr = src[scramble_table[i % scramble_table.len()]] ^ content_key[i % content_key.len()];
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_x2m() {
|
||||
let mut buffer = *include_bytes!("__fixture__/x2m_hdr.bin");
|
||||
let expected = *include_bytes!("__fixture__/x2m_hdr_plain.bin");
|
||||
let buffer_slice: &mut [u8] = &mut buffer;
|
||||
decrypt_android(FileType::X2M, buffer_slice.try_into().unwrap());
|
||||
assert_eq!(expected, buffer);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_x3m() {
|
||||
let mut buffer = *include_bytes!("__fixture__/x3m_hdr.bin");
|
||||
let expected = *include_bytes!("__fixture__/x3m_hdr_plain.bin");
|
||||
let buffer_slice: &mut [u8] = &mut buffer;
|
||||
decrypt_android(FileType::X3M, buffer_slice.try_into().unwrap());
|
||||
assert_eq!(expected, buffer);
|
||||
}
|
||||
}
|
||||
43
um_crypto/xmly/src/lib.rs
Normal file
43
um_crypto/xmly/src/lib.rs
Normal file
@@ -0,0 +1,43 @@
|
||||
use cipher::block_padding::UnpadError;
|
||||
use cipher::InvalidLength;
|
||||
use thiserror::Error;
|
||||
use umc_utils::base64::DecodeError;
|
||||
|
||||
pub mod android;
|
||||
pub mod pc;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum XmlyError {
|
||||
#[error("Expected ID3 metadata")]
|
||||
MetadataMissing,
|
||||
|
||||
#[error("ID3 Metadata too small (require {0} bytes)")]
|
||||
MetadataTooSmall(usize),
|
||||
|
||||
#[error("Failed to extract encrypted audio segment size")]
|
||||
ParseHeaderSizeError,
|
||||
|
||||
#[error("Failed to extract Stage 1 IV data")]
|
||||
ParseStage1IVError,
|
||||
|
||||
#[error("Failed to extract Stage 2 decryption key")]
|
||||
ParseStage2KeyError,
|
||||
|
||||
#[error("Failed to extract audio header")]
|
||||
ParseAudioHeaderError,
|
||||
|
||||
#[error("Decryption stage 1 failed (padding)")]
|
||||
DecryptStage1Error(UnpadError),
|
||||
|
||||
#[error("Decryption stage 1 failed (b64 decode)")]
|
||||
DecryptStage1B64Error(DecodeError),
|
||||
|
||||
#[error("Decryption stage 2 failed (init)")]
|
||||
InitStage2Error(InvalidLength),
|
||||
|
||||
#[error("Decryption stage 2 failed (padding)")]
|
||||
DecryptStage2Error(UnpadError),
|
||||
|
||||
#[error("Decryption stage 2 failed (b64 decode)")]
|
||||
DecryptStage2B64Error(DecodeError),
|
||||
}
|
||||
196
um_crypto/xmly/src/pc.rs
Normal file
196
um_crypto/xmly/src/pc.rs
Normal file
@@ -0,0 +1,196 @@
|
||||
use crate::XmlyError;
|
||||
use aes::cipher::generic_array::GenericArray;
|
||||
use aes::cipher::BlockDecryptMut;
|
||||
use aes::{Aes192Dec, Aes256Dec};
|
||||
use byteorder::{ByteOrder, BE};
|
||||
use cbc::cipher::KeyIvInit;
|
||||
use cbc::Decryptor;
|
||||
use cipher::block_padding::Pkcs7;
|
||||
use umc_utils::base64;
|
||||
|
||||
type Aes256CbcDec = Decryptor<Aes256Dec>;
|
||||
type Aes192CbcDec = Decryptor<Aes192Dec>;
|
||||
|
||||
fn parse_safe_sync_int(v: u32) -> u32 {
|
||||
((v & 0x7f00_0000) >> 3)
|
||||
| ((v & 0x007f_0000) >> 2)
|
||||
| ((v & 0x0000_7f00) >> 1)
|
||||
| (v & 0x0000_007f)
|
||||
}
|
||||
|
||||
fn from_unicode(buf: &[u8]) -> String {
|
||||
let data = buf
|
||||
.iter()
|
||||
.step_by(2)
|
||||
.map_while(|&b| match b {
|
||||
0 => None,
|
||||
b => Some(b),
|
||||
})
|
||||
.collect::<Vec<u8>>();
|
||||
String::from_utf8_lossy(&data[..]).to_string()
|
||||
}
|
||||
|
||||
pub struct Header {
|
||||
pub data_start_offset: usize,
|
||||
pub encrypted_header_size: usize,
|
||||
stage1_iv: [u8; 0x10],
|
||||
stage2_key: [u8; 0x18],
|
||||
m4a_header: Vec<u8>,
|
||||
}
|
||||
|
||||
impl Header {
|
||||
pub fn from_buffer(buffer: &[u8]) -> Result<Self, XmlyError> {
|
||||
if buffer.len() < 10 {
|
||||
Err(XmlyError::MetadataTooSmall(10))?;
|
||||
}
|
||||
if &buffer[0..3] != b"ID3" {
|
||||
Err(XmlyError::MetadataMissing)?;
|
||||
}
|
||||
|
||||
let header_size = parse_safe_sync_int(BE::read_u32(&buffer[6..10]));
|
||||
let data_start_offset = 10 + header_size as usize;
|
||||
|
||||
if buffer.len() < data_start_offset {
|
||||
Err(XmlyError::MetadataTooSmall(data_start_offset))?;
|
||||
}
|
||||
|
||||
let mut encrypted_header_size = None;
|
||||
let mut stage1_iv = None;
|
||||
let mut stage2_key = None;
|
||||
let mut m4a_header = None;
|
||||
|
||||
let mut offset = 10;
|
||||
while offset < data_start_offset {
|
||||
// Safety check
|
||||
if offset + 10 > buffer.len() {
|
||||
break;
|
||||
}
|
||||
|
||||
let tag_name: &[u8; 4] = &buffer[offset..offset + 4].try_into().unwrap();
|
||||
offset += 4;
|
||||
|
||||
let tag_value_size = BE::read_u32(&buffer[offset..offset + 4]) as usize;
|
||||
offset += 4;
|
||||
|
||||
// flags, ignore
|
||||
offset += 2;
|
||||
|
||||
if offset + tag_value_size > buffer.len() {
|
||||
Err(XmlyError::MetadataTooSmall(offset + tag_value_size))?;
|
||||
}
|
||||
let data = &buffer[offset + 3..offset + tag_value_size];
|
||||
offset += tag_value_size;
|
||||
|
||||
match tag_name {
|
||||
b"TSIZ" => {
|
||||
let data = from_unicode(data)
|
||||
.parse::<usize>()
|
||||
.map_err(|_| XmlyError::ParseHeaderSizeError)?;
|
||||
encrypted_header_size = Some(data);
|
||||
}
|
||||
b"TSRC" | b"TENC" => {
|
||||
let data = hex::decode(from_unicode(data))
|
||||
.map_err(|_| XmlyError::ParseStage1IVError)?
|
||||
.try_into()
|
||||
.map_err(|_| XmlyError::ParseStage1IVError)?;
|
||||
stage1_iv = Some(data)
|
||||
}
|
||||
b"TSSE" => {
|
||||
let data = base64::decode(from_unicode(data))
|
||||
.map_err(|_| XmlyError::ParseAudioHeaderError)?;
|
||||
m4a_header = Some(data);
|
||||
}
|
||||
b"TRCK" => {
|
||||
let data = from_unicode(data);
|
||||
let tmp = data.as_bytes();
|
||||
|
||||
let mut key = *b"123456781234567812345678";
|
||||
key[24 - tmp.len()..].copy_from_slice(tmp);
|
||||
stage2_key = Some(key);
|
||||
}
|
||||
// ignore unknown tags
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
data_start_offset,
|
||||
encrypted_header_size: encrypted_header_size.ok_or(XmlyError::ParseHeaderSizeError)?,
|
||||
stage1_iv: stage1_iv.ok_or(XmlyError::ParseStage1IVError)?,
|
||||
stage2_key: stage2_key.ok_or(XmlyError::ParseStage2KeyError)?,
|
||||
m4a_header: m4a_header.ok_or(XmlyError::ParseAudioHeaderError)?,
|
||||
})
|
||||
}
|
||||
|
||||
const STAGE1_KEY: [u8; 32] = *b"ximalayaximalayaximalayaximalaya";
|
||||
fn decrypt_stage1<'a>(&self, buffer: &'a mut [u8]) -> Result<&'a [u8], XmlyError> {
|
||||
let key = GenericArray::from(Self::STAGE1_KEY);
|
||||
let iv = GenericArray::from(self.stage1_iv);
|
||||
let aes = Aes256CbcDec::new(&key, &iv);
|
||||
|
||||
let len = aes
|
||||
.decrypt_padded_mut::<Pkcs7>(buffer)
|
||||
.map_err(XmlyError::DecryptStage1Error)?
|
||||
.len();
|
||||
base64::decode_overwrite(&mut buffer[..len]).map_err(XmlyError::DecryptStage1B64Error)
|
||||
}
|
||||
|
||||
fn decrypt_stage2<'a>(&self, buffer: &'a mut [u8]) -> Result<&'a [u8], XmlyError> {
|
||||
let key = &self.stage2_key[..24];
|
||||
let iv = &self.stage2_key[..16];
|
||||
let aes = Aes192CbcDec::new_from_slices(key, iv).map_err(XmlyError::InitStage2Error)?;
|
||||
|
||||
let len = aes
|
||||
.decrypt_padded_mut::<Pkcs7>(buffer)
|
||||
.map_err(XmlyError::DecryptStage2Error)?
|
||||
.len();
|
||||
base64::decode_overwrite(&mut buffer[..len]).map_err(XmlyError::DecryptStage2B64Error)
|
||||
}
|
||||
|
||||
pub fn decrypt<'a>(&self, buffer: &'a mut [u8]) -> Result<&'a [u8], XmlyError> {
|
||||
let len = self.decrypt_stage1(&mut buffer[..])?.len();
|
||||
self.decrypt_stage2(&mut buffer[..len])
|
||||
}
|
||||
|
||||
pub fn copy_m4a_header(&self) -> Vec<u8> {
|
||||
self.m4a_header.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::pc::Header;
|
||||
use crate::XmlyError;
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
|
||||
#[test]
|
||||
fn test_sample_xm() -> Result<(), XmlyError> {
|
||||
let manifest_dir = env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is not set");
|
||||
let sample_path = format!("{}/src/__fixture__/sample.xm", manifest_dir);
|
||||
let sample_out_path = format!("{}/src/__fixture__/sample.m4a", manifest_dir);
|
||||
|
||||
println!("decrypt {} -> {}", sample_path, sample_out_path);
|
||||
|
||||
if let Ok(mut xm) = fs::read(sample_path) {
|
||||
let file = match Header::from_buffer(&xm[..1024]) {
|
||||
Err(XmlyError::MetadataTooSmall(n)) => Header::from_buffer(&xm[..n])?,
|
||||
Err(err) => Err(err)?,
|
||||
Ok(x) => x,
|
||||
};
|
||||
|
||||
let (_hdr, buffer) = xm.split_at_mut(file.data_start_offset);
|
||||
let (buffer, plain) = buffer.split_at_mut(file.encrypted_header_size);
|
||||
|
||||
let decrypted = file.decrypt(buffer)?;
|
||||
let mut f_out = File::create(sample_out_path).expect("can't open test output file");
|
||||
f_out.write_all(&file.copy_m4a_header()).expect("header");
|
||||
f_out.write_all(decrypted).expect("decrypted part");
|
||||
f_out.write_all(plain).expect("plain part");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user