Compare commits

..

10 Commits

8 changed files with 40 additions and 18 deletions

View File

@ -11,6 +11,8 @@
<sourceFolder url="file://$MODULE_DIR$/um_crypto/ncm/src" isTestSource="false" /> <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_audio/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/pkg" />
</content> </content>
<orderEntry type="inheritedJdk" /> <orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" /> <orderEntry type="sourceFolder" forTests="false" />

View File

@ -7,7 +7,7 @@ use core::{KuwoDes, Mode};
use umc_utils::base64; use umc_utils::base64;
/// Decrypt string content /// Decrypt string content
pub fn decrypt_ksing(data: &str, key: &[u8; 8]) -> Result<String> { pub fn decrypt_ksing<T: AsRef<[u8]>>(data: T, key: &[u8; 8]) -> Result<String> {
let mut decoded = base64::decode(data)?; let mut decoded = base64::decode(data)?;
let des = KuwoDes::new(key, Mode::Decrypt); let des = KuwoDes::new(key, Mode::Decrypt);
@ -30,7 +30,7 @@ pub fn encrypt_ksing<T: AsRef<[u8]>>(data: T, key: &[u8; 8]) -> Result<String> {
Ok(base64::encode(data)) Ok(base64::encode(data))
} }
pub fn decode_ekey(data: &str, key: &[u8; 8]) -> Result<String> { pub fn decode_ekey<T: AsRef<[u8]>>(data: T, key: &[u8; 8]) -> Result<String> {
let decoded = decrypt_ksing(data, key)?; let decoded = decrypt_ksing(data, key)?;
Ok(decoded[16..].to_string()) Ok(decoded[16..].to_string())
} }

View File

@ -11,7 +11,10 @@ const KEY: [u8; 0x20] = [
impl CipherV1 { impl CipherV1 {
pub fn new(resource_id: u32) -> Self { pub fn new(resource_id: u32) -> Self {
let mut key = KEY; let mut key = KEY;
for (k, r) in key.iter_mut().zip(resource_id.to_string().as_bytes()) { for (k, r) in key
.iter_mut()
.zip(resource_id.to_string().as_bytes().iter().cycle())
{
*k ^= r; *k ^= r;
} }

View File

@ -1,6 +1,7 @@
use crate::kwm_v1::CipherV1; use crate::kwm_v1::CipherV1;
use anyhow::Result; use anyhow::Result;
use byteorder::{ReadBytesExt, LE}; use byteorder::{ReadBytesExt, LE};
use std::fmt;
use std::io::{Cursor, Read}; use std::io::{Cursor, Read};
use thiserror::Error; use thiserror::Error;
use umc_qmc::QMCv2Cipher; use umc_qmc::QMCv2Cipher;
@ -13,13 +14,16 @@ pub use umc_qmc::QMCv2Cipher as CipherV2;
/// Commonly used secret key for Kuwo services. /// Commonly used secret key for Kuwo services.
pub const SECRET_KEY: [u8; 8] = *b"ylzsxkwm"; pub const SECRET_KEY: [u8; 8] = *b"ylzsxkwm";
#[derive(Debug)]
pub struct HeaderMagicBytes(pub [u8; 16]);
#[derive(Error, Debug)] #[derive(Error, Debug)]
pub enum KuwoCryptoError { pub enum KuwoCryptoError {
#[error("Invalid DES data size (expected: {0} mod 8 == 0)")] #[error("Invalid DES data size (expected: {0} mod 8 == 0)")]
InvalidDesDataSize(usize), InvalidDesDataSize(usize),
#[error("Invalid KWM header magic bytes: {0:?}")] #[error("Invalid KWM header magic bytes: {0}")]
InvalidHeaderMagic([u8; 16]), InvalidHeaderMagic(HeaderMagicBytes),
#[error("KWMv2: EKey required")] #[error("KWMv2: EKey required")]
V2EKeyRequired, V2EKeyRequired,
@ -28,6 +32,15 @@ pub enum KuwoCryptoError {
UnsupportedVersion(usize), UnsupportedVersion(usize),
} }
impl fmt::Display for HeaderMagicBytes {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for byte in self.0.iter() {
write!(f, "{:02x} ", byte)?;
}
Ok(())
}
}
pub const DATA_START_OFFSET: usize = 0x400; pub const DATA_START_OFFSET: usize = 0x400;
pub enum Cipher { pub enum Cipher {
@ -78,8 +91,8 @@ impl Header {
let mut format_name = [0u8; 0x0C]; let mut format_name = [0u8; 0x0C];
cursor.read_exact(&mut format_name)?; cursor.read_exact(&mut format_name)?;
if magic != Self::MAGIC_1 || magic != Self::MAGIC_2 { if magic != Self::MAGIC_1 && magic != Self::MAGIC_2 {
Err(KuwoCryptoError::InvalidHeaderMagic(magic))?; Err(KuwoCryptoError::InvalidHeaderMagic(HeaderMagicBytes(magic)))?;
} }
Ok(Self { Ok(Self {
@ -99,7 +112,7 @@ impl Header {
let cipher = match self.version { let cipher = match self.version {
1 => Cipher::V1(CipherV1::new(self.resource_id)), 1 => Cipher::V1(CipherV1::new(self.resource_id)),
2 => match ekey { 2 => match ekey {
Some(ekey) => Cipher::V2(CipherV2::new(ekey)?), Some(ekey) => Cipher::V2(CipherV2::new_from_ekey(ekey)?),
None => Err(KuwoCryptoError::V2EKeyRequired)?, None => Err(KuwoCryptoError::V2EKeyRequired)?,
}, },
version => Err(KuwoCryptoError::UnsupportedVersion(version as usize))?, version => Err(KuwoCryptoError::UnsupportedVersion(version as usize))?,
@ -121,9 +134,9 @@ impl Header {
pub struct CipherBoDian(QMCv2Cipher); pub struct CipherBoDian(QMCv2Cipher);
impl CipherBoDian { impl CipherBoDian {
pub fn new(ekey: &str) -> Result<Self> { pub fn new<T: AsRef<[u8]>>(ekey: T) -> Result<Self> {
let ekey = des::decode_ekey(&ekey, &SECRET_KEY)?; let ekey = des::decode_ekey(ekey, &SECRET_KEY)?;
let cipher = CipherV2::new(ekey)?; let cipher = CipherV2::new_from_ekey(ekey.as_str())?;
Ok(Self(cipher)) Ok(Self(cipher))
} }

View File

@ -42,7 +42,7 @@ fn make_simple_key<const N: usize>() -> [u8; N] {
result result
} }
pub fn decrypt_v1(ekey: &[u8]) -> Result<Box<[u8]>> { pub fn decrypt_v1(ekey: &[u8]) -> Result<Vec<u8>> {
if ekey.len() < 12 { if ekey.len() < 12 {
Err(EKeyDecryptError::EKeyTooShort)?; Err(EKeyDecryptError::EKeyTooShort)?;
} }
@ -58,10 +58,10 @@ pub fn decrypt_v1(ekey: &[u8]) -> Result<Box<[u8]>> {
.collect_vec(); .collect_vec();
let plaintext = tc_tea::decrypt(cipher, tea_key).map_err(EKeyDecryptError::FailDecryptV1)?; let plaintext = tc_tea::decrypt(cipher, tea_key).map_err(EKeyDecryptError::FailDecryptV1)?;
Ok([header, &plaintext].concat().into()) Ok([header, &plaintext].concat())
} }
pub fn decrypt_v2(ekey: &[u8]) -> Result<Box<[u8]>> { pub fn decrypt_v2(ekey: &[u8]) -> Result<Vec<u8>> {
let ekey = base64::decode(ekey)?; let ekey = base64::decode(ekey)?;
let ekey = tc_tea::decrypt(ekey, EKEY_V2_KEY1).map_err(EKeyDecryptError::FailDecryptV2)?; let ekey = tc_tea::decrypt(ekey, EKEY_V2_KEY1).map_err(EKeyDecryptError::FailDecryptV2)?;
let ekey = tc_tea::decrypt(ekey, EKEY_V2_KEY2).map_err(EKeyDecryptError::FailDecryptV2)?; let ekey = tc_tea::decrypt(ekey, EKEY_V2_KEY2).map_err(EKeyDecryptError::FailDecryptV2)?;
@ -70,7 +70,7 @@ pub fn decrypt_v2(ekey: &[u8]) -> Result<Box<[u8]>> {
decrypt_v1(&ekey) decrypt_v1(&ekey)
} }
pub fn decrypt<T: AsRef<[u8]>>(ekey: T) -> Result<Box<[u8]>> { pub fn decrypt<T: AsRef<[u8]>>(ekey: T) -> Result<Vec<u8>> {
let ekey = ekey.as_ref(); let ekey = ekey.as_ref();
match ekey.strip_prefix(EKEY_V2_PREFIX) { match ekey.strip_prefix(EKEY_V2_PREFIX) {
Some(v2_ekey) => decrypt_v2(v2_ekey), Some(v2_ekey) => decrypt_v2(v2_ekey),

View File

@ -35,6 +35,11 @@ impl QMCv2Cipher {
Ok(cipher) Ok(cipher)
} }
pub fn new_from_ekey<T: AsRef<[u8]>>(ekey_str: T) -> Result<Self> {
let key = ekey::decrypt(ekey_str)?;
Self::new(key)
}
pub fn decrypt<T>(&self, data: &mut T, offset: usize) pub fn decrypt<T>(&self, data: &mut T, offset: usize)
where where
T: AsMut<[u8]> + ?Sized, T: AsMut<[u8]> + ?Sized,

View File

@ -19,8 +19,7 @@ impl JsQMC2 {
/// Create a new QMC2 (mgg/mflac) cipher instance. /// Create a new QMC2 (mgg/mflac) cipher instance.
#[wasm_bindgen(constructor)] #[wasm_bindgen(constructor)]
pub fn new(ekey: &str) -> Result<JsQMC2, JsError> { pub fn new(ekey: &str) -> Result<JsQMC2, JsError> {
let key = umc_qmc::ekey::decrypt(ekey).map_err(map_js_error)?; let cipher = QMCv2Cipher::new_from_ekey(ekey).map_err(map_js_error)?;
let cipher = QMCv2Cipher::new(key).map_err(map_js_error)?;
Ok(JsQMC2(cipher)) Ok(JsQMC2(cipher))
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "@unlock-music/crypto", "name": "@unlock-music/crypto",
"version": "0.0.0-alpha.10", "version": "0.0.0-alpha.11",
"description": "Project Unlock Music: 加解密支持库", "description": "Project Unlock Music: 加解密支持库",
"scripts": { "scripts": {
"build": "node build.js", "build": "node build.js",