mirror of
https://git.um-react.app/um/lib_um_crypto_rust.git
synced 2026-03-08 04:29:54 +00:00
[qtfm] feat #4: implement QingTingFM decipher
This commit is contained in:
12
um_crypto/qtfm/Cargo.toml
Normal file
12
um_crypto/qtfm/Cargo.toml
Normal file
@@ -0,0 +1,12 @@
|
||||
[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" }
|
||||
30
um_crypto/qtfm/src/lib.rs
Normal file
30
um_crypto/qtfm/src/lib.rs
Normal file
@@ -0,0 +1,30 @@
|
||||
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[..]);
|
||||
}
|
||||
}
|
||||
58
um_crypto/qtfm/src/nonce.rs
Normal file
58
um_crypto/qtfm/src/nonce.rs
Normal file
@@ -0,0 +1,58 @@
|
||||
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(())
|
||||
}
|
||||
53
um_crypto/qtfm/src/secret.rs
Normal file
53
um_crypto/qtfm/src/secret.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
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,6 +9,12 @@ pub const ENGINE: Base64Engine = Base64Engine::new(
|
||||
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
|
||||
where
|
||||
T: AsRef<[u8]>,
|
||||
@@ -33,3 +39,17 @@ where
|
||||
data[..len].copy_from_slice(&decoded);
|
||||
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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user