Compare commits
No commits in common. "8f00373dbf73b2da20d5d7777523b509cab0639e" and "a149b4e4690b4dc7b84fb275cbb08451fad10c4f" have entirely different histories.
8f00373dbf
...
a149b4e469
@ -14,7 +14,6 @@
|
|||||||
<sourceFolder url="file://$MODULE_DIR$/um_crypto/joox/src" isTestSource="false" />
|
<sourceFolder url="file://$MODULE_DIR$/um_crypto/joox/src" isTestSource="false" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/um_crypto/xmly/src" isTestSource="false" />
|
<sourceFolder url="file://$MODULE_DIR$/um_crypto/xmly/src" isTestSource="false" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/um_crypto/xiami/src" isTestSource="false" />
|
<sourceFolder url="file://$MODULE_DIR$/um_crypto/xiami/src" isTestSource="false" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/um_crypto/qtfm/src" isTestSource="false" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/target" />
|
<excludeFolder url="file://$MODULE_DIR$/target" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/um_wasm_loader/dist" />
|
<excludeFolder url="file://$MODULE_DIR$/um_wasm_loader/dist" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/um_wasm_loader/pkg" />
|
<excludeFolder url="file://$MODULE_DIR$/um_wasm_loader/pkg" />
|
||||||
|
23
Cargo.lock
generated
23
Cargo.lock
generated
@ -228,15 +228,6 @@ dependencies = [
|
|||||||
"typenum",
|
"typenum",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ctr"
|
|
||||||
version = "0.9.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835"
|
|
||||||
dependencies = [
|
|
||||||
"cipher",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "diff"
|
name = "diff"
|
||||||
version = "0.1.13"
|
version = "0.1.13"
|
||||||
@ -531,13 +522,11 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"clap",
|
"clap",
|
||||||
"hex",
|
|
||||||
"umc_joox",
|
"umc_joox",
|
||||||
"umc_kgm",
|
"umc_kgm",
|
||||||
"umc_kuwo",
|
"umc_kuwo",
|
||||||
"umc_ncm",
|
"umc_ncm",
|
||||||
"umc_qmc",
|
"umc_qmc",
|
||||||
"umc_qtfm",
|
|
||||||
"umc_utils",
|
"umc_utils",
|
||||||
"umc_xiami",
|
"umc_xiami",
|
||||||
"umc_xmly",
|
"umc_xmly",
|
||||||
@ -627,18 +616,6 @@ dependencies = [
|
|||||||
"umc_utils",
|
"umc_utils",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "umc_qtfm"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"aes",
|
|
||||||
"byteorder",
|
|
||||||
"cbc",
|
|
||||||
"ctr",
|
|
||||||
"thiserror",
|
|
||||||
"umc_utils",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "umc_utils"
|
name = "umc_utils"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
@ -11,8 +11,6 @@ umc_kgm = { path = "../um_crypto/kgm" }
|
|||||||
umc_kuwo = { path = "../um_crypto/kuwo" }
|
umc_kuwo = { path = "../um_crypto/kuwo" }
|
||||||
umc_ncm = { path = "../um_crypto/ncm" }
|
umc_ncm = { path = "../um_crypto/ncm" }
|
||||||
umc_qmc = { path = "../um_crypto/qmc" }
|
umc_qmc = { path = "../um_crypto/qmc" }
|
||||||
umc_qtfm = { path = "../um_crypto/qtfm" }
|
|
||||||
umc_xiami = { path = "../um_crypto/xiami" }
|
umc_xiami = { path = "../um_crypto/xiami" }
|
||||||
umc_xmly = { path = "../um_crypto/xmly" }
|
umc_xmly = { path = "../um_crypto/xmly" }
|
||||||
umc_utils = { path = "../um_crypto/utils" }
|
umc_utils = { path = "../um_crypto/utils" }
|
||||||
hex = "0.4.3"
|
|
||||||
|
@ -5,7 +5,6 @@ pub mod kgm;
|
|||||||
pub mod ncm;
|
pub mod ncm;
|
||||||
pub mod qmc1;
|
pub mod qmc1;
|
||||||
pub mod qmc2;
|
pub mod qmc2;
|
||||||
mod qtfm;
|
|
||||||
pub mod xiami;
|
pub mod xiami;
|
||||||
pub mod xmly;
|
pub mod xmly;
|
||||||
|
|
||||||
@ -25,6 +24,4 @@ pub enum Commands {
|
|||||||
XMLY(xmly::ArgsXimalaya),
|
XMLY(xmly::ArgsXimalaya),
|
||||||
#[command(name = "xiami")]
|
#[command(name = "xiami")]
|
||||||
Xiami(xiami::ArgsXiami),
|
Xiami(xiami::ArgsXiami),
|
||||||
#[command(name = "qtfm")]
|
|
||||||
QTFM(qtfm::ArgsQingTingFM),
|
|
||||||
}
|
}
|
||||||
|
@ -1,100 +0,0 @@
|
|||||||
use crate::Cli;
|
|
||||||
use anyhow::{bail, Result};
|
|
||||||
use clap::Args;
|
|
||||||
use std::fs::File;
|
|
||||||
use std::io::{BufReader, BufWriter, Read, Write};
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use umc_qtfm::nonce::make_decipher_iv;
|
|
||||||
use umc_qtfm::Decipher;
|
|
||||||
|
|
||||||
/// Decrypt a qta file (QingTingFM)
|
|
||||||
#[derive(Args)]
|
|
||||||
pub struct ArgsQingTingFM {
|
|
||||||
/// Path to output file, e.g. /export/Music/song.flac
|
|
||||||
#[clap(short, long)]
|
|
||||||
output: PathBuf,
|
|
||||||
|
|
||||||
/// Path to input file, e.g. /export/Music/song.xm
|
|
||||||
#[arg(name = "input")]
|
|
||||||
input: PathBuf,
|
|
||||||
|
|
||||||
/// Device info (CSV), in the order of "product,device,manufacturer,brand,board,model".
|
|
||||||
#[clap(short, long = "device")]
|
|
||||||
device: Option<String>,
|
|
||||||
|
|
||||||
/// Device key, in hex.
|
|
||||||
#[clap(short = 'k', long = "device-key")]
|
|
||||||
device_key: Option<String>,
|
|
||||||
|
|
||||||
/// Override name. default to file name.
|
|
||||||
#[clap(short, long)]
|
|
||||||
name: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ArgsQingTingFM {
|
|
||||||
pub fn run(&self, cli: &Cli) -> Result<i32> {
|
|
||||||
let mut reader = BufReader::with_capacity(cli.buffer_size, File::open(&self.input)?);
|
|
||||||
let mut writer = BufWriter::with_capacity(cli.buffer_size, File::create(&self.output)?);
|
|
||||||
|
|
||||||
let file_name = match &self.name {
|
|
||||||
Some(x) => x.clone(),
|
|
||||||
None => match self.input.file_name() {
|
|
||||||
Some(x) => x.to_string_lossy().to_string(),
|
|
||||||
None => bail!("failed to get file name"),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
let device_key = self.make_device_key()?;
|
|
||||||
let iv = make_decipher_iv(file_name)?;
|
|
||||||
|
|
||||||
let decipher = Decipher::new(&device_key, &iv);
|
|
||||||
|
|
||||||
let mut offset = 0;
|
|
||||||
let mut buffer = vec![0u8; cli.buffer_size];
|
|
||||||
loop {
|
|
||||||
let n = reader.read(&mut buffer[..])?;
|
|
||||||
if n == 0 {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
decipher.decrypt(&mut buffer[..n], offset);
|
|
||||||
writer.write_all(&buffer[..n])?;
|
|
||||||
offset += n;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn make_device_key(&self) -> Result<[u8; 0x10]> {
|
|
||||||
let key = match &self.device_key {
|
|
||||||
Some(key) => {
|
|
||||||
let mut result = [0u8; 0x10];
|
|
||||||
hex::decode_to_slice(key, &mut result)?;
|
|
||||||
result
|
|
||||||
}
|
|
||||||
None => match &self.device {
|
|
||||||
Some(device) => {
|
|
||||||
let mut device_parts = vec![];
|
|
||||||
for item in device.split(',') {
|
|
||||||
device_parts.push(item);
|
|
||||||
}
|
|
||||||
if device_parts.len() != 6 {
|
|
||||||
bail!(
|
|
||||||
"device needs 6 parts. current: {} part(s)",
|
|
||||||
device_parts.len()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
umc_qtfm::secret::make_device_secret(
|
|
||||||
device_parts[0],
|
|
||||||
device_parts[1],
|
|
||||||
device_parts[2],
|
|
||||||
device_parts[3],
|
|
||||||
device_parts[4],
|
|
||||||
device_parts[5],
|
|
||||||
)
|
|
||||||
}
|
|
||||||
None => bail!("one of device/device-key is required."),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
Ok(key)
|
|
||||||
}
|
|
||||||
}
|
|
@ -34,7 +34,6 @@ fn run_command(cli: &Cli) -> Result<i32> {
|
|||||||
Some(Commands::JOOX(cmd)) => cmd.run(&cli),
|
Some(Commands::JOOX(cmd)) => cmd.run(&cli),
|
||||||
Some(Commands::XMLY(cmd)) => cmd.run(&cli),
|
Some(Commands::XMLY(cmd)) => cmd.run(&cli),
|
||||||
Some(Commands::Xiami(cmd)) => cmd.run(&cli),
|
Some(Commands::Xiami(cmd)) => cmd.run(&cli),
|
||||||
Some(Commands::QTFM(cmd)) => cmd.run(&cli),
|
|
||||||
None => {
|
None => {
|
||||||
// https://github.com/clap-rs/clap/issues/3857#issuecomment-1161796261
|
// https://github.com/clap-rs/clap/issues/3857#issuecomment-1161796261
|
||||||
todo!("implement a sensible default command, similar to um/cli");
|
todo!("implement a sensible default command, similar to um/cli");
|
||||||
|
@ -1,12 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "umc_qtfm"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
aes = "0.8.4"
|
|
||||||
byteorder = "1.5.0"
|
|
||||||
cbc = "0.1.2"
|
|
||||||
ctr = "0.9.2"
|
|
||||||
thiserror = "1.0.63"
|
|
||||||
umc_utils = { path = "../utils" }
|
|
@ -1,30 +0,0 @@
|
|||||||
use aes::cipher::{KeyIvInit, StreamCipher, StreamCipherSeek};
|
|
||||||
use thiserror::Error;
|
|
||||||
use umc_utils::base64::DecodeError;
|
|
||||||
|
|
||||||
pub mod nonce;
|
|
||||||
pub mod secret;
|
|
||||||
|
|
||||||
#[derive(Error, Debug, Clone)]
|
|
||||||
pub enum QingTingFMError {
|
|
||||||
#[error("Failed to decode file name.")]
|
|
||||||
DecodeFileNameFailed(DecodeError),
|
|
||||||
|
|
||||||
#[error("File name does not start with known prefix.")]
|
|
||||||
MissingPrefix,
|
|
||||||
}
|
|
||||||
|
|
||||||
type Aes128Ctr64BE = ctr::Ctr64BE<aes::Aes128>;
|
|
||||||
pub struct Decipher(Aes128Ctr64BE);
|
|
||||||
|
|
||||||
impl Decipher {
|
|
||||||
pub fn new(device_key: &[u8; 0x10], iv: &[u8; 0x10]) -> Self {
|
|
||||||
Decipher(Aes128Ctr64BE::new(device_key.into(), iv.into()))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn decrypt(&self, buffer: &mut [u8], offset: usize) {
|
|
||||||
let mut aes_engine = self.0.clone();
|
|
||||||
aes_engine.seek(offset);
|
|
||||||
aes_engine.apply_keystream(&mut buffer[..]);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,58 +0,0 @@
|
|||||||
use crate::QingTingFMError;
|
|
||||||
use byteorder::{ByteOrder, BE};
|
|
||||||
use umc_utils::base64;
|
|
||||||
|
|
||||||
fn hash_resource_id(resource_id: &[u8]) -> i64 {
|
|
||||||
resource_id.iter().fold(0, |sum, &chr| {
|
|
||||||
let outer_sum = sum ^ (chr as i64);
|
|
||||||
|
|
||||||
[0, 1, 4, 5, 7, 8, 40]
|
|
||||||
.iter()
|
|
||||||
.fold(0, |sum, &shl| sum.wrapping_add(outer_sum.wrapping_shl(shl)))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn make_decipher_iv<S: AsRef<[u8]>>(file_path_or_name: S) -> Result<[u8; 16], QingTingFMError> {
|
|
||||||
let path = file_path_or_name.as_ref();
|
|
||||||
let name = match path.iter().rposition(|&b| b == b'\\' || b == b'/') {
|
|
||||||
Some(n) => &path[n..],
|
|
||||||
None => path,
|
|
||||||
};
|
|
||||||
let name = name.strip_suffix(b".qta").unwrap_or(name);
|
|
||||||
|
|
||||||
let resource_info = if let Some(x) = name.strip_prefix(b".p!") {
|
|
||||||
base64::decode(x).map_err(QingTingFMError::DecodeFileNameFailed)?
|
|
||||||
} else if let Some(x) = name.strip_prefix(b".p~!") {
|
|
||||||
base64::decode_url_safe(x).map_err(QingTingFMError::DecodeFileNameFailed)?
|
|
||||||
} else {
|
|
||||||
Err(QingTingFMError::MissingPrefix)?
|
|
||||||
};
|
|
||||||
|
|
||||||
// We only need the resource id part.
|
|
||||||
let resource_id = match resource_info.iter().position(|&b| b == b'@') {
|
|
||||||
None => &resource_info[..],
|
|
||||||
Some(n) => &resource_info[..n],
|
|
||||||
};
|
|
||||||
|
|
||||||
let hash = hash_resource_id(resource_id);
|
|
||||||
let mut iv = [0u8; 0x10];
|
|
||||||
BE::write_i64(&mut iv[..8], hash);
|
|
||||||
Ok(iv)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_make_nonce_key() -> Result<(), QingTingFMError> {
|
|
||||||
let actual1 = make_decipher_iv(".p!MTIzNDU2.qta")?; // "123456"
|
|
||||||
let expected1 = [0x4c, 0x43, 0x18, 0xd9, 0x98, 0xe6, 0xef, 0x57];
|
|
||||||
assert_eq!(&actual1[..8], &expected1);
|
|
||||||
|
|
||||||
let actual2 = make_decipher_iv(".p!OTg3NjU0MzIx.qta")?; // "987654321"
|
|
||||||
let expected2 = [0x32, 0xef, 0xa8, 0xef, 0x16, 0xc4, 0x98, 0x33];
|
|
||||||
assert_eq!(&actual2[..8], &expected2);
|
|
||||||
|
|
||||||
let actual3 = make_decipher_iv(".p~!MTIzNED_-w==.qta")?; // "1234@\xff\xfb"
|
|
||||||
let expected3 = [0x2e, 0x08, 0x09, 0x99, 0x62, 0x7a, 0xea, 0xac];
|
|
||||||
assert_eq!(&actual3[..8], &expected3);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
@ -1,53 +0,0 @@
|
|||||||
fn java_string_hash_code<T: AsRef<[u8]>>(s: T) -> u32 {
|
|
||||||
let mut hash = 0u32;
|
|
||||||
|
|
||||||
for &chr in s.as_ref() {
|
|
||||||
hash = hash.wrapping_mul(31).wrapping_add(chr as u32);
|
|
||||||
}
|
|
||||||
|
|
||||||
hash
|
|
||||||
}
|
|
||||||
|
|
||||||
const DEVICE_KEY_SALT: [u8; 0x10] = [
|
|
||||||
0x26, 0x2b, 0x2b, 0x12, 0x11, 0x12, 0x14, 0x0a, 0x08, 0x00, 0x08, 0x0a, 0x14, 0x12, 0x11, 0x12,
|
|
||||||
];
|
|
||||||
pub fn make_device_secret<S: AsRef<[u8]>>(
|
|
||||||
product: S,
|
|
||||||
device: S,
|
|
||||||
manufacturer: S,
|
|
||||||
brand: S,
|
|
||||||
board: S,
|
|
||||||
model: S,
|
|
||||||
) -> [u8; 0x10] {
|
|
||||||
let device_id_hash_code = [product, device, manufacturer, brand, board, model]
|
|
||||||
.iter()
|
|
||||||
.fold(0u32, |sum, value| {
|
|
||||||
sum.wrapping_add(java_string_hash_code(value))
|
|
||||||
});
|
|
||||||
let device_id_hash_code_hex = format!("{:x}", device_id_hash_code);
|
|
||||||
let device_id_hash_code_hex = device_id_hash_code_hex.as_bytes();
|
|
||||||
|
|
||||||
let mut device_key = [0u8; 0x10];
|
|
||||||
device_key[..device_id_hash_code_hex.len()].copy_from_slice(&device_id_hash_code_hex);
|
|
||||||
for (key, salt) in device_key.iter_mut().zip(DEVICE_KEY_SALT) {
|
|
||||||
*key = salt.wrapping_add(*key);
|
|
||||||
}
|
|
||||||
device_key
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_secret_generation() {
|
|
||||||
let actual = make_device_secret(
|
|
||||||
"product",
|
|
||||||
"device",
|
|
||||||
"manufacturer",
|
|
||||||
"brand",
|
|
||||||
"board",
|
|
||||||
"model",
|
|
||||||
);
|
|
||||||
let expected = [
|
|
||||||
0x59, 0x64, 0x91, 0x77, 0x45, 0x46, 0x75, 0x6d, 0x08, 0x00, 0x08, 0x0a, 0x14, 0x12, 0x11,
|
|
||||||
0x12,
|
|
||||||
];
|
|
||||||
assert_eq!(actual, expected);
|
|
||||||
}
|
|
@ -9,12 +9,6 @@ pub const ENGINE: Base64Engine = Base64Engine::new(
|
|||||||
GeneralPurposeConfig::new().with_decode_padding_mode(DecodePaddingMode::Indifferent),
|
GeneralPurposeConfig::new().with_decode_padding_mode(DecodePaddingMode::Indifferent),
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Don't add padding when encoding, and require no padding when decoding.
|
|
||||||
pub const ENGINE_URL_SAFE: Base64Engine = Base64Engine::new(
|
|
||||||
&alphabet::URL_SAFE,
|
|
||||||
GeneralPurposeConfig::new().with_decode_padding_mode(DecodePaddingMode::Indifferent),
|
|
||||||
);
|
|
||||||
|
|
||||||
pub fn encode<T>(data: T) -> String
|
pub fn encode<T>(data: T) -> String
|
||||||
where
|
where
|
||||||
T: AsRef<[u8]>,
|
T: AsRef<[u8]>,
|
||||||
@ -39,17 +33,3 @@ where
|
|||||||
data[..len].copy_from_slice(&decoded);
|
data[..len].copy_from_slice(&decoded);
|
||||||
Ok(&data[..len])
|
Ok(&data[..len])
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn encode_url_safe<T>(data: T) -> String
|
|
||||||
where
|
|
||||||
T: AsRef<[u8]>,
|
|
||||||
{
|
|
||||||
ENGINE_URL_SAFE.encode(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn decode_url_safe<T>(data: T) -> Result<Vec<u8>, DecodeError>
|
|
||||||
where
|
|
||||||
T: AsRef<[u8]>,
|
|
||||||
{
|
|
||||||
ENGINE_URL_SAFE.decode(data)
|
|
||||||
}
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@unlock-music/crypto",
|
"name": "@unlock-music/crypto",
|
||||||
"version": "0.0.0-alpha.15",
|
"version": "0.0.0-alpha.14",
|
||||||
"description": "Project Unlock Music: 加解密支持库",
|
"description": "Project Unlock Music: 加解密支持库",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "node build.js",
|
"build": "node build.js",
|
||||||
|
7
um_wasm_loader/src/loader.d.ts
vendored
7
um_wasm_loader/src/loader.d.ts
vendored
@ -1,9 +1,2 @@
|
|||||||
export * from '../pkg/um_wasm';
|
export * from '../pkg/um_wasm';
|
||||||
|
|
||||||
/**
|
|
||||||
* Get package version.
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
export function getUmcVersion(): string;
|
|
||||||
|
|
||||||
export const ready: Promise<void>;
|
export const ready: Promise<void>;
|
||||||
|
@ -21,6 +21,10 @@ function loader() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get package version.
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
export function getUmcVersion() {
|
export function getUmcVersion() {
|
||||||
return process.env.UMC_VERSION;
|
return process.env.UMC_VERSION;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user