[joox] feat #1: add joox decipher implementation
This commit is contained in:
parent
b3fc9f8318
commit
12199616c2
@ -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
54
Cargo.lock
generated
@ -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"
|
||||
|
@ -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
49
um_cli/src/cmd/joox.rs
Normal 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)
|
||||
}
|
||||
}
|
@ -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),
|
||||
}
|
||||
|
@ -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
15
um_crypto/joox/Cargo.toml
Normal 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" }
|
34
um_crypto/joox/src/decrypt.rs
Normal file
34
um_crypto/joox/src/decrypt.rs
Normal 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)
|
||||
}
|
||||
}
|
75
um_crypto/joox/src/header.rs
Normal file
75
um_crypto/joox/src/header.rs
Normal 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
28
um_crypto/joox/src/lib.rs
Normal 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),
|
||||
}
|
@ -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" }
|
||||
|
29
um_wasm/src/exports/joox.rs
Normal file
29
um_wasm/src/exports/joox.rs
Normal 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())
|
||||
}
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
pub mod audio;
|
||||
pub mod joox;
|
||||
pub mod kgm;
|
||||
pub mod kuwo;
|
||||
pub mod ncm;
|
||||
|
Loading…
Reference in New Issue
Block a user