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;