diff --git a/.idea/lib_um_crypto.iml b/.idea/lib_um_crypto.iml index 3dbbad2..9e64d5e 100644 --- a/.idea/lib_um_crypto.iml +++ b/.idea/lib_um_crypto.iml @@ -11,6 +11,7 @@ + diff --git a/Cargo.lock b/Cargo.lock index a64a0bd..c686f80 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/um_cli/Cargo.toml b/um_cli/Cargo.toml index 849154f..f90cafd 100644 --- a/um_cli/Cargo.toml +++ b/um_cli/Cargo.toml @@ -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" } diff --git a/um_cli/src/cmd/joox.rs b/um_cli/src/cmd/joox.rs new file mode 100644 index 0000000..a1858b2 --- /dev/null +++ b/um_cli/src/cmd/joox.rs @@ -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 { + 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) + } +} diff --git a/um_cli/src/cmd/mod.rs b/um_cli/src/cmd/mod.rs index 69e390c..f467d2d 100644 --- a/um_cli/src/cmd/mod.rs +++ b/um_cli/src/cmd/mod.rs @@ -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), } diff --git a/um_cli/src/main.rs b/um_cli/src/main.rs index d60d847..14d9c0e 100644 --- a/um_cli/src/main.rs +++ b/um_cli/src/main.rs @@ -31,6 +31,7 @@ fn run_command(cli: &Cli) -> Result { 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"); diff --git a/um_crypto/joox/Cargo.toml b/um_crypto/joox/Cargo.toml new file mode 100644 index 0000000..d5a5a26 --- /dev/null +++ b/um_crypto/joox/Cargo.toml @@ -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" } diff --git a/um_crypto/joox/src/decrypt.rs b/um_crypto/joox/src/decrypt.rs new file mode 100644 index 0000000..dfa8cba --- /dev/null +++ b/um_crypto/joox/src/decrypt.rs @@ -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::(buffer) + .map_err(JooxError::AesUnpadError)?; + + Ok(result) + } +} diff --git a/um_crypto/joox/src/header.rs b/um_crypto/joox/src/header.rs new file mode 100644 index 0000000..800cee3 --- /dev/null +++ b/um_crypto/joox/src/header.rs @@ -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>(install_guid: T) -> [u8; 0x10] { + let mut derived_key = [0u8; 0x20]; + pbkdf2::>( + 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>(buffer: &[u8], device_guid: T) -> Result { + 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) + } +} diff --git a/um_crypto/joox/src/lib.rs b/um_crypto/joox/src/lib.rs new file mode 100644 index 0000000..c8c4eed --- /dev/null +++ b/um_crypto/joox/src/lib.rs @@ -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), +} diff --git a/um_wasm/Cargo.toml b/um_wasm/Cargo.toml index 4a09d40..516a116 100644 --- a/um_wasm/Cargo.toml +++ b/um_wasm/Cargo.toml @@ -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" } diff --git a/um_wasm/src/exports/joox.rs b/um_wasm/src/exports/joox.rs new file mode 100644 index 0000000..8243906 --- /dev/null +++ b/um_wasm/src/exports/joox.rs @@ -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 { + 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 { + let decrypted = self.0.decrypt_audio_block(buffer).map_err(JsError::from)?; + Ok(decrypted.len()) + } +} diff --git a/um_wasm/src/exports/mod.rs b/um_wasm/src/exports/mod.rs index 2d7a5dc..5b63cdd 100644 --- a/um_wasm/src/exports/mod.rs +++ b/um_wasm/src/exports/mod.rs @@ -1,4 +1,5 @@ pub mod audio; +pub mod joox; pub mod kgm; pub mod kuwo; pub mod ncm;