use std::time::{SystemTime, UNIX_EPOCH};
use aes::cipher::{BlockEncryptMut, KeyIvInit, block_padding::Pkcs7};
use anyhow::{Context, Result};
use base64::{Engine, engine::general_purpose::STANDARD as B64};
use md5::{Digest, Md5};
use rand::RngExt;
use rsa::{Pkcs1v15Encrypt, RsaPublicKey, pkcs8::DecodePublicKey, rand_core::OsRng};
use serde_json::{Value, json};
const ENDPOINT: &str = "https://api.tencentmusic.com/tme/trpc/proxy";
const SECRET: &str = "ZdJqM15EeO2zWc08";
const APP_KEY: &str = "0AND0HD6FE4HY80F";
const APP_VERSION: &str = "13.2.5.8";
const PUBLIC_KEY_DER_B64: &str = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDEIxgwoutfwoJxcGQeedgP7FG9qaIuS0qzfR8gWkrkTZKM2iWHn2ajQpBRZjMSoSf6+KJGvar2ORhBfpDXyVtZCKpqLQ+FLkpncClKVIrBwv6PHyUvuCb0rIarmgDnzkfQAqVufEtR64iazGDKatvJ9y6B9NMbHddGSAUmRTCrHQIDAQAB";
const HEADER_SIGN_SALT: &str = "qimei_qq_androidpzAuCmaFAaFaHrdakPjLIEqKrGnSOOvH";
const KEY_CHARS: &[u8] = b"adbcdef1234567890";
type Aes128CbcEnc = cbc::Encryptor<aes::Aes128>;
pub async fn obtain(client: &reqwest::Client) -> Result<String> {
let ts = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
let payload = random_payload(ts);
let crypt_key = random_chars(KEY_CHARS, 16);
let nonce = random_chars(KEY_CHARS, 16);
let pk = public_key()?;
let key = B64.encode(pk.encrypt(&mut OsRng, Pkcs1v15Encrypt, crypt_key.as_bytes())?);
let params = B64.encode(aes_cbc_encrypt(
crypt_key.as_bytes(),
&serde_json::to_vec(&payload)?,
));
let extra = format!(r#"{{"appKey":"{APP_KEY}"}}"#);
let ts_ms = (ts * 1000).to_string();
let sign = md5_hex(&[&key, ¶ms, &ts_ms, &nonce, SECRET, &extra]);
let body = json!({
"app": 0,
"os": 1,
"qimeiParams": {
"key": key, "params": params, "time": ts.to_string(),
"nonce": nonce, "sign": sign, "extra": extra,
}
});
let resp: Value = client
.post(ENDPOINT)
.header("method", "GetQimei")
.header("service", "trpc.tme_datasvr.qimeiproxy.QimeiProxy")
.header("appid", "qimei_qq_android")
.header("sign", md5_hex(&[HEADER_SIGN_SALT, &ts.to_string()]))
.header("user-agent", "QQMusic")
.header("timestamp", ts.to_string())
.json(&body)
.send()
.await?
.error_for_status()?
.json()
.await?;
let inner: Value = serde_json::from_str(
resp.get("data")
.and_then(Value::as_str)
.context("qimei 响应缺少 data")?,
)?;
inner
.pointer("/data/q36")
.and_then(Value::as_str)
.filter(|s| !s.is_empty())
.map(str::to_string)
.context("qimei 响应缺少 q36")
}
fn public_key() -> Result<RsaPublicKey> {
let der = B64.decode(PUBLIC_KEY_DER_B64)?;
RsaPublicKey::from_public_key_der(&der).context("解析 qimei 公钥失败")
}
fn aes_cbc_encrypt(key: &[u8], data: &[u8]) -> Vec<u8> {
Aes128CbcEnc::new(key.into(), key.into()).encrypt_padded_vec_mut::<Pkcs7>(data)
}
fn md5_hex(parts: &[&str]) -> String {
let mut h = Md5::new();
for p in parts {
h.update(p.as_bytes());
}
format!("{:x}", h.finalize())
}
fn random_chars(charset: &[u8], n: usize) -> String {
let mut rng = rand::rng();
(0..n)
.map(|_| charset[rng.random_range(0..charset.len())] as char)
.collect()
}
fn random_hex_bytes(n: usize) -> String {
let mut rng = rand::rng();
(0..n)
.map(|_| format!("{:02x}", rng.random::<u8>()))
.collect()
}
fn random_imei() -> String {
let mut rng = rand::rng();
let digits: Vec<u32> = (0..14).map(|_| rng.random_range(0..10)).collect();
imei_with_check(&digits)
}
fn imei_with_check(digits: &[u32]) -> String {
let sum: u32 = digits
.iter()
.enumerate()
.map(|(i, &x)| {
if i % 2 == 0 {
x * 2 / 10 + x * 2 % 10
} else {
x
}
})
.sum();
let mut s: String = digits
.iter()
.map(|d| char::from_digit(*d, 10).unwrap())
.collect();
s.push(char::from_digit(sum * 9 % 10, 10).unwrap());
s
}
fn civil_from_unix(secs: u64) -> (i64, u32, u32, u32, u32, u32) {
let secs = secs as i64 + 8 * 3600;
let days = secs.div_euclid(86400);
let rem = secs.rem_euclid(86400);
let z = days + 719_468;
let era = z.div_euclid(146_097);
let doe = z - era * 146_097;
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146_096) / 365;
let y = yoe + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = (doy - (153 * mp + 2) / 5 + 1) as u32;
let m = if mp < 10 { mp + 3 } else { mp - 9 } as u32;
let y = if m <= 2 { y + 1 } else { y };
(
y,
m,
d,
(rem / 3600) as u32,
(rem % 3600 / 60) as u32,
(rem % 60) as u32,
)
}
fn random_beacon_id(ts: u64) -> String {
let mut rng = rand::rng();
let (y, m, ..) = civil_from_unix(ts);
let time_month = format!("{y:04}-{m:02}-01");
let rand1: u32 = rng.random_range(100_000..=999_999);
let rand2: u32 = rng.random_range(100_000_000..=999_999_999);
const SAME: [u32; 16] = [1, 2, 13, 14, 17, 18, 21, 22, 25, 26, 29, 30, 33, 34, 37, 38];
let mut out = String::new();
for i in 1..=40u32 {
if SAME.contains(&i) {
out += &format!("k{i}:{time_month}{rand1}.{rand2}");
} else if i == 3 {
out += "k3:0000000000000000";
} else if i == 4 {
out += &format!("k4:{}", random_chars(b"123456789abcdef", 16));
} else {
out += &format!("k{i}:{}", rng.random_range(0..=9999));
}
out.push(';');
}
out
}
fn random_payload(ts: u64) -> Value {
let mut rng = rand::rng();
let proc_version = format!(
"Linux 5.4.0-54-generic-{} (android-build@google.com)",
random_chars(
b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789",
8
)
);
let fixed_rand: u64 = rng.random_range(0..=14400);
let (y, mo, d, h, mi, s) = civil_from_unix(ts.saturating_sub(fixed_rand));
let reserved = json!({
"harmony": "0", "oz": "UhYmelwouA+V2nPWbOvLTgN2/m8jwGB+yUB5v9tysQg=",
"oo": "Xecjt+9S1+f8Pz2VLSxgpw==", "kelong": "0",
"uptimes": format!("{y:04}-{mo:02}-{d:02} {h:02}:{mi:02}:{s:02}"),
"clone": "0", "containe": "", "multiUser": "0", "bod": "Xiaomi", "dv": "sagit",
"firstLevel": "", "manufact": "Xiaomi", "name": "MI 6", "host": "se.infra",
"kernel": proc_version,
});
json!({
"androidId": random_hex_bytes(8), "platformId": 1, "appKey": APP_KEY,
"appVersion": APP_VERSION, "beaconIdSrc": random_beacon_id(ts), "brand": "Xiaomi",
"channelId": "10003505", "cid": "", "imei": random_imei(), "imsi": "", "mac": "",
"model": "MI 6", "networkType": "unknown", "oaid": "",
"osVersion": "Android 10,level 29", "qimei": "", "qimei36": "",
"sdkVersion": "1.2.13.6", "targetSdkVersion": "33", "audit": "", "userId": "{}",
"packageId": "com.tencent.qqmusic", "deviceType": "Phone", "sdkName": "",
"reserved": reserved.to_string(),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn md5_concat() {
assert_eq!(md5_hex(&["a", "bc"]), "900150983cd24fb0d6963f7d28e17f72");
}
#[test]
fn aes_cbc_pkcs7() {
let out = aes_cbc_encrypt(b"0123456789abcdef", b"0123456789abcdef");
assert_eq!(out.len(), 32);
assert_eq!(aes_cbc_encrypt(b"0123456789abcdef", b"x").len(), 16);
}
#[test]
fn imei_check_digit() {
assert_eq!(imei_with_check(&[0; 14]), "000000000000000");
let digits = [4, 9, 0, 1, 5, 4, 2, 0, 3, 2, 3, 7, 5, 1];
assert_eq!(imei_with_check(&digits), "490154203237510");
assert_eq!(random_imei().len(), 15);
}
#[test]
fn civil_date() {
assert_eq!(civil_from_unix(0), (1970, 1, 1, 8, 0, 0));
assert_eq!(civil_from_unix(1_791_446_400), (2026, 10, 8, 16, 0, 0));
assert_eq!(
civil_from_unix(1_709_164_800 - 8 * 3600),
(2024, 2, 29, 0, 0, 0)
);
}
#[test]
fn beacon_shape() {
let b = random_beacon_id(1_791_446_400);
assert_eq!(b.matches(';').count(), 40);
assert!(b.starts_with("k1:2026-10-01"));
assert!(b.contains(";k3:0000000000000000;"));
}
#[test]
fn public_key_parses() {
public_key().unwrap();
}
}