[joox] feat #1: add joox decipher implementation

This commit is contained in:
鲁树人 2024-09-17 21:45:51 +01:00
parent b3fc9f8318
commit 12199616c2
13 changed files with 293 additions and 1 deletions

View File

@ -11,6 +11,7 @@
<sourceFolder url="file://$MODULE_DIR$/um_crypto/ncm/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/um_audio/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/um_crypto/kgm/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/um_crypto/joox/src" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/target" />
<excludeFolder url="file://$MODULE_DIR$/um_wasm_loader/dist" />
<excludeFolder url="file://$MODULE_DIR$/um_wasm_loader/pkg" />

54
Cargo.lock generated
View File

@ -233,6 +233,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"crypto-common",
"subtle",
]
[[package]]
@ -270,6 +271,15 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "hmac"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
dependencies = [
"digest",
]
[[package]]
name = "inout"
version = "0.1.3"
@ -348,6 +358,16 @@ version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
name = "pbkdf2"
version = "0.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2"
dependencies = [
"digest",
"hmac",
]
[[package]]
name = "pretty_assertions"
version = "1.4.0"
@ -397,6 +417,17 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
[[package]]
name = "sha1"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "shlex"
version = "1.3.0"
@ -409,6 +440,12 @@ version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "subtle"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
[[package]]
name = "syn"
version = "2.0.77"
@ -470,6 +507,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"clap",
"umc_joox",
"umc_kgm",
"umc_kuwo",
"umc_ncm",
@ -485,6 +523,7 @@ dependencies = [
"console_error_panic_hook",
"getrandom",
"um_audio",
"umc_joox",
"umc_kgm",
"umc_kuwo",
"umc_ncm",
@ -493,6 +532,21 @@ dependencies = [
"wasm-bindgen-test",
]
[[package]]
name = "umc_joox"
version = "0.1.0"
dependencies = [
"aes",
"byteorder",
"cipher",
"hmac",
"pbkdf2",
"sha1",
"thiserror",
"umc_qmc",
"umc_utils",
]
[[package]]
name = "umc_kgm"
version = "0.1.0"

View File

@ -6,8 +6,9 @@ edition = "2021"
[dependencies]
anyhow = "1.0.86"
clap = { version = "4.5.17", features = ["derive"] }
umc_kuwo = { path = "../um_crypto/kuwo" }
umc_joox = { path = "../um_crypto/joox" }
umc_kgm = { path = "../um_crypto/kgm" }
umc_kuwo = { path = "../um_crypto/kuwo" }
umc_ncm = { path = "../um_crypto/ncm" }
umc_qmc = { path = "../um_crypto/qmc" }
umc_utils = { path = "../um_crypto/utils" }

49
um_cli/src/cmd/joox.rs Normal file
View File

@ -0,0 +1,49 @@
use crate::Cli;
use anyhow::Result;
use clap::Args;
use std::fs::File;
use std::io::{BufReader, BufWriter, Read, Seek, SeekFrom, Write};
use std::path::PathBuf;
use umc_joox::decrypt::JooxDecipher;
use umc_joox::header::Header;
/// Decrypt a ofl_en file (Joox Music, Encryption V4)
#[derive(Args)]
pub struct ArgsJoox {
/// Path to output file, e.g. /export/Music/song.flac
#[clap(short, long)]
output: PathBuf,
/// Path to input file, e.g. /export/Music/song.ofl_en
#[arg(name = "input")]
input: PathBuf,
/// Device GUID
#[clap(short = 'u', long)]
device_guid: String,
}
impl ArgsJoox {
pub fn run(&self, _cli: &Cli) -> Result<i32> {
let mut file_input = File::open(&self.input)?;
let mut header_buffer = [0u8; 0x0c];
file_input.read_exact(&mut header_buffer)?;
let header = Header::from_buffer(&header_buffer, &self.device_guid)?;
file_input.seek(SeekFrom::Start(header.audio_start_offset as u64))?;
let reader = BufReader::new(file_input);
let mut input_stream = reader.take(header.original_file_len);
let file_output = File::create(&self.output)?;
let mut writer = BufWriter::new(file_output);
let mut buffer = vec![0u8; header.get_audio_block_size()].into_boxed_slice();
while let Ok(()) = input_stream.read_exact(&mut buffer) {
let result = header.decrypt_audio_block(&mut buffer)?;
writer.write_all(result)?;
}
Ok(0)
}
}

View File

@ -1,5 +1,6 @@
use clap::Subcommand;
pub mod joox;
pub mod kgm;
pub mod ncm;
pub mod qmc1;
@ -15,4 +16,6 @@ pub enum Commands {
NCM(ncm::ArgsNCM),
#[command(name = "kgm")]
KGM(kgm::ArgsKGM),
#[command(name = "joox")]
JOOX(joox::ArgsJoox),
}

View File

@ -31,6 +31,7 @@ fn run_command(cli: &Cli) -> Result<i32> {
Some(Commands::QMCv2(cmd)) => cmd.run(&cli),
Some(Commands::NCM(cmd)) => cmd.run(&cli),
Some(Commands::KGM(cmd)) => cmd.run(&cli),
Some(Commands::JOOX(cmd)) => cmd.run(&cli),
None => {
// https://github.com/clap-rs/clap/issues/3857#issuecomment-1161796261
todo!("implement a sensible default command, similar to um/cli");

15
um_crypto/joox/Cargo.toml Normal file
View File

@ -0,0 +1,15 @@
[package]
name = "umc_joox"
version = "0.1.0"
edition = "2021"
[dependencies]
aes = "0.8.4"
byteorder = "1.5.0"
cipher = "0.4.4"
hmac = "0.12.1"
pbkdf2 = "0.12.2"
sha1 = "0.10.5"
thiserror = "1.0.63"
umc_qmc = { path = "../qmc" }
umc_utils = { path = "../utils" }

View File

@ -0,0 +1,34 @@
use crate::header::Header;
use crate::JooxError;
use cipher::block_padding::Pkcs7;
use cipher::BlockDecryptMut;
use std::cmp::max;
pub trait JooxDecipher {
fn get_audio_block_size(&self) -> usize;
fn decrypt_audio_block<'a>(&self, buffer: &'a mut [u8]) -> Result<&'a [u8], JooxError>;
}
impl JooxDecipher for Header {
fn get_audio_block_size(&self) -> usize {
max(
self.audio_encrypted_block_size,
self.audio_decrypted_block_size,
)
}
fn decrypt_audio_block<'a>(&self, buffer: &'a mut [u8]) -> Result<&'a [u8], JooxError> {
let buffer_size = self.get_audio_block_size();
let (buffer, _) = buffer
.split_at_mut_checked(buffer_size)
.ok_or_else(|| JooxError::OutputBufferTooSmall(buffer_size))?;
let result = (&self.aes_engine)
.decrypt_padded_mut::<Pkcs7>(buffer)
.map_err(JooxError::AesUnpadError)?;
Ok(result)
}
}

View File

@ -0,0 +1,75 @@
use crate::JooxError;
use aes::Aes128;
use byteorder::{ByteOrder, BE};
use cipher::generic_array::GenericArray;
use cipher::KeyInit;
use hmac::Hmac;
use pbkdf2::pbkdf2;
use sha1::Sha1;
pub struct Header {
pub version: u8,
pub original_file_len: u64,
pub audio_start_offset: usize,
pub audio_encrypted_block_size: usize,
pub audio_decrypted_block_size: usize,
pub aes_engine: Aes128,
}
const V4_PASSWORD_SALT: [u8; 0x10] = [
0xA4, 0x0B, 0xC8, 0x34, 0xD6, 0x95, 0xF3, 0x13, 0x23, 0x23, 0x43, 0x23, 0x54, 0x63, 0x83, 0xF3,
];
fn v4_generate_password<T: AsRef<[u8]>>(install_guid: T) -> [u8; 0x10] {
let mut derived_key = [0u8; 0x20];
pbkdf2::<Hmac<Sha1>>(
install_guid.as_ref(),
&V4_PASSWORD_SALT,
1000,
&mut derived_key,
)
.expect("buffer setup incorrect");
let mut result = [0u8; 0x10];
result.copy_from_slice(&derived_key[..0x10]);
result
}
impl Header {
pub fn from_buffer<T: AsRef<[u8]>>(buffer: &[u8], device_guid: T) -> Result<Self, JooxError> {
if buffer.len() < 0x0c {
Err(JooxError::HeaderTooSmall(0x0c))?;
}
let mut magic = [0u8; 4];
magic.copy_from_slice(&buffer[..4]);
let version = match magic {
[b'E', b'!', b'0', version] => version,
magic => Err(JooxError::NotJooxHeader(magic))?,
};
let result = match version {
b'4' => {
let original_file_len = BE::read_u64(&buffer[4..0x0c]);
let audio_start_offset = 0x0c;
let password = v4_generate_password(device_guid);
let aes_engine = Aes128::new(&GenericArray::from(password));
let audio_encrypted_block_size = 0x100010;
let audio_decrypted_block_size = 0x100000;
Self {
version,
original_file_len,
audio_start_offset,
audio_encrypted_block_size,
audio_decrypted_block_size,
aes_engine,
}
}
ver => Err(JooxError::UnsupportedJooxVersion(ver))?,
};
Ok(result)
}
}

28
um_crypto/joox/src/lib.rs Normal file
View File

@ -0,0 +1,28 @@
use cipher::block_padding::UnpadError;
use cipher::inout::NotEqualError;
use thiserror::Error;
pub mod decrypt;
pub mod header;
#[derive(Error, Debug, Clone)]
pub enum JooxError {
#[error("Header too small, require at least {0} bytes.")]
HeaderTooSmall(usize),
#[error("Output buffer require at least {0} bytes.")]
OutputBufferTooSmall(usize),
#[error("Input buffer require at least {0} bytes.")]
InputBufferTooSmall(usize),
#[error("Not Joox encrypted header: {0:2x?}")]
NotJooxHeader([u8; 4]),
#[error("Unsupported Joox version: {0:2x}")]
UnsupportedJooxVersion(u8),
#[error("AES Decryption Unpad Error: {0}")]
AesUnpadError(UnpadError),
#[error("AES Buffer setup error: {0}")]
AesBufferSetupError(NotEqualError),
}

View File

@ -23,6 +23,7 @@ getrandom = { version = "0.2", features = ["js"] }
# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for
# code size when deploying.
console_error_panic_hook = { version = "0.1.7", optional = true }
umc_joox = { path = "../um_crypto/joox" }
umc_kgm = { path = "../um_crypto/kgm" }
umc_kuwo = { path = "../um_crypto/kuwo" }
umc_ncm = { path = "../um_crypto/ncm" }

View File

@ -0,0 +1,29 @@
use umc_joox::decrypt::JooxDecipher;
use umc_joox::header::Header;
use wasm_bindgen::prelude::wasm_bindgen;
use wasm_bindgen::JsError;
/// Joox file.
#[wasm_bindgen(js_name=JooxFile)]
pub struct JsJooxFile(Header);
#[wasm_bindgen(js_class = JooxFile)]
impl JsJooxFile {
/// Initialize header. Header should be 0x0c bytes.
pub fn parse(header: &[u8], uuid: String) -> Result<JsJooxFile, JsError> {
Ok(JsJooxFile(
Header::from_buffer(header, uuid.as_bytes()).map_err(JsError::from)?,
))
}
#[wasm_bindgen(getter, js_name = "bufferLength")]
pub fn get_buffer_size(&self) -> usize {
self.0.get_audio_block_size()
}
#[wasm_bindgen(js_name = "decrypt")]
pub fn decrypt(&self, buffer: &mut [u8]) -> Result<usize, JsError> {
let decrypted = self.0.decrypt_audio_block(buffer).map_err(JsError::from)?;
Ok(decrypted.len())
}
}

View File

@ -1,4 +1,5 @@
pub mod audio;
pub mod joox;
pub mod kgm;
pub mod kuwo;
pub mod ncm;