Files
Nekosonic-Music/src-tauri/src/music_sources/kwdes.rs
T
atdunbg 1fddbeab77 feat: v0.9.0 多音源系统、歌曲标签、搜索建议、播放器改进
音源系统:
- 新增可配置多音源(波点/酷狗/酷我),支持启用/禁用/排序/自定义源
- 酷我 kwDES 加密 + mobi.s 接口绕过 VIP 限制
- VIP/版权歌曲跨源 fallback,版本一致性匹配(原版/DJ/Live/混音)
- 多源聚合搜索,歌名+歌手组合去重

歌曲标签:
- 付费/音质/版本/音源标签,网易云 fee 字段精确判断
- privilege 数组合并(歌手/歌单/专辑/每日推荐/收藏页)

搜索建议:
- 120ms 防抖 + compositionend 支持,中文输入实时建议
- Teleport + fixed 定位避免父容器裁剪

播放器:
- 网络流 seek 支持(SharedBuffer 下载完成后 byte_len 可用)
- 切歌停止旧播放再加载新歌,进度条起步归零
- 切歌加载状态可视化,按钮禁用反馈
- tick generation 防竞态,旧 tick 不覆盖新歌位置
- 系统媒体控制(跨平台,Windows 增加 hwnd 就绪检查避免 panic)

其他:
- 设置迁移 v7(恢复酷我音源)
- useUpdater 模块级共享状态
2026-07-02 09:54:45 +08:00

301 lines
9.6 KiB
Rust

//! kwDES 加密算法 —— 酷我音乐 mobi.s 接口的 query 加密
//! 移植自 UnblockNeteaseMusic/server 的 kwDES.js
//! 参考: https://github.com/UnblockNeteaseMusic/server/blob/enhanced/src/kwDES.js
// EXPANSION (E 扩展置换, 64个元素, 32->48位)
const ARRAY_E: [i32; 64] = [
31, 0, 1, 2, 3, 4, -1, -1, 3, 4, 5, 6, 7, 8, -1, -1,
7, 8, 9, 10, 11, 12, -1, -1, 11, 12, 13, 14, 15, 16, -1, -1,
15, 16, 17, 18, 19, 20, -1, -1, 19, 20, 21, 22, 23, 24, -1, -1,
23, 24, 25, 26, 27, 28, -1, -1, 27, 28, 29, 30, 31, 30, -1, -1,
];
// INITIAL_PERMUTATION (IP 初始置换, 64个元素)
const ARRAY_IP: [i32; 64] = [
57, 49, 41, 33, 25, 17, 9, 1,
59, 51, 43, 35, 27, 19, 11, 3,
61, 53, 45, 37, 29, 21, 13, 5,
63, 55, 47, 39, 31, 23, 15, 7,
56, 48, 40, 32, 24, 16, 8, 0,
58, 50, 42, 34, 26, 18, 10, 2,
60, 52, 44, 36, 28, 20, 12, 4,
62, 54, 46, 38, 30, 22, 14, 6,
];
// INVERSE_PERMUTATION (IP-1 逆置换, 64个元素)
const ARRAY_IP_1: [i32; 64] = [
39, 7, 47, 15, 55, 23, 63, 31,
38, 6, 46, 14, 54, 22, 62, 30,
37, 5, 45, 13, 53, 21, 61, 29,
36, 4, 44, 12, 52, 20, 60, 28,
35, 3, 43, 11, 51, 19, 59, 27,
34, 2, 42, 10, 50, 18, 58, 26,
33, 1, 41, 9, 49, 17, 57, 25,
32, 0, 40, 8, 48, 16, 56, 24,
];
// ROTATES (循环移位表, 16个元素)
const ARRAY_LS: [u32; 16] = [
1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1,
];
// 移位掩码
const ARRAY_LS_MASK: [u64; 3] = [0, 0x100001, 0x300003];
// PERMUTATION (P 置换, 32个元素)
const ARRAY_P: [i32; 32] = [
15, 6, 19, 20, 28, 11, 27, 16,
0, 14, 22, 25, 4, 17, 30, 9,
1, 7, 23, 13, 31, 26, 2, 8,
18, 12, 29, 5, 21, 10, 3, 24,
];
// PERMUTED_CHOICE1 (PC-1, 56个元素)
const ARRAY_PC_1: [i32; 56] = [
56, 48, 40, 32, 24, 16, 8,
0, 57, 49, 41, 33, 25, 17,
9, 1, 58, 50, 42, 34, 26,
18, 10, 2, 59, 51, 43, 35,
62, 54, 46, 38, 30, 22, 14,
6, 61, 53, 45, 37, 29, 21,
13, 5, 60, 52, 44, 36, 28,
20, 12, 4, 27, 19, 11, 3,
];
// PERMUTED_CHOICE2 (PC-2, 64个元素, -1表示跳过)
const ARRAY_PC_2: [i32; 64] = [
13, 16, 10, 23, 0, 4, -1, -1,
2, 27, 14, 5, 20, 9, -1, -1,
22, 18, 11, 3, 25, 7, -1, -1,
15, 6, 26, 19, 12, 1, -1, -1,
40, 51, 30, 36, 46, 54, -1, -1,
29, 39, 50, 44, 32, 47, -1, -1,
43, 48, 38, 55, 33, 52, -1, -1,
45, 41, 49, 35, 28, 31, -1, -1,
];
// S-Box (8个子表, 每个64个元素)
const MATRIX_NS_BOX: [[u8; 64]; 8] = [
[
14, 4, 3, 15, 2, 13, 5, 3, 13, 14, 6, 9, 11, 2, 0, 5, 4, 1, 10, 12, 15,
6, 9, 10, 1, 8, 12, 7, 8, 11, 7, 0, 0, 15, 10, 5, 14, 4, 9, 10, 7, 8,
12, 3, 13, 1, 3, 6, 15, 12, 6, 11, 2, 9, 5, 0, 4, 2, 11, 14, 1, 7, 8,
13,
],
[
15, 0, 9, 5, 6, 10, 12, 9, 8, 7, 2, 12, 3, 13, 5, 2, 1, 14, 7, 8, 11, 4,
0, 3, 14, 11, 13, 6, 4, 1, 10, 15, 3, 13, 12, 11, 15, 3, 6, 0, 4, 10, 1,
7, 8, 4, 11, 14, 13, 8, 0, 6, 2, 15, 9, 5, 7, 1, 10, 12, 14, 2, 5, 9,
],
[
10, 13, 1, 11, 6, 8, 11, 5, 9, 4, 12, 2, 15, 3, 2, 14, 0, 6, 13, 1, 3,
15, 4, 10, 14, 9, 7, 12, 5, 0, 8, 7, 13, 1, 2, 4, 3, 6, 12, 11, 0, 13,
5, 14, 6, 8, 15, 2, 7, 10, 8, 15, 4, 9, 11, 5, 9, 0, 14, 3, 10, 7, 1,
12,
],
[
7, 10, 1, 15, 0, 12, 11, 5, 14, 9, 8, 3, 9, 7, 4, 8, 13, 6, 2, 1, 6, 11,
12, 2, 3, 0, 5, 14, 10, 13, 15, 4, 13, 3, 4, 9, 6, 10, 1, 12, 11, 0, 2,
5, 0, 13, 14, 2, 8, 15, 7, 4, 15, 1, 10, 7, 5, 6, 12, 11, 3, 8, 9, 14,
],
[
2, 4, 8, 15, 7, 10, 13, 6, 4, 1, 3, 12, 11, 7, 14, 0, 12, 2, 5, 9, 10,
13, 0, 3, 1, 11, 15, 5, 6, 8, 9, 14, 14, 11, 5, 6, 4, 1, 3, 10, 2, 12,
15, 0, 13, 2, 8, 5, 11, 8, 0, 15, 7, 14, 9, 4, 12, 7, 10, 9, 1, 13, 6,
3,
],
[
12, 9, 0, 7, 9, 2, 14, 1, 10, 15, 3, 4, 6, 12, 5, 11, 1, 14, 13, 0, 2,
8, 7, 13, 15, 5, 4, 10, 8, 3, 11, 6, 10, 4, 6, 11, 7, 9, 0, 6, 4, 2, 13,
1, 9, 15, 3, 8, 15, 3, 1, 14, 12, 5, 11, 0, 2, 12, 14, 7, 5, 10, 8, 13,
],
[
4, 1, 3, 10, 15, 12, 5, 0, 2, 11, 9, 6, 8, 7, 6, 9, 11, 4, 12, 15, 0, 3,
10, 5, 14, 13, 7, 8, 13, 14, 1, 2, 13, 6, 14, 9, 4, 1, 2, 14, 11, 13, 5,
0, 1, 10, 8, 3, 0, 11, 3, 5, 9, 4, 15, 2, 7, 8, 12, 15, 10, 7, 6, 12,
],
[
13, 7, 10, 0, 6, 9, 5, 15, 8, 4, 3, 10, 11, 14, 12, 5, 2, 11, 9, 6, 15,
12, 0, 3, 4, 1, 14, 13, 1, 2, 7, 8, 1, 2, 12, 15, 10, 4, 0, 3, 13, 14,
6, 9, 7, 8, 9, 6, 15, 1, 5, 12, 3, 10, 14, 5, 8, 7, 11, 0, 4, 13, 2, 11,
],
];
/// 按位置换:从 l 中按 arr 的索引取位,放到结果的对应位置
fn bit_transform(arr: &[i32], l: u64) -> u64 {
let mut result: u64 = 0;
for (i, &idx) in arr.iter().enumerate() {
if idx < 0 {
continue;
}
if l & (1u64 << idx as u64) != 0 {
result |= 1u64 << i as u64;
}
}
result
}
/// 生成 16 个子密钥
fn sub_keys(l: u64, mode: i32) -> [u64; 16] {
let mut l2 = bit_transform(&ARRAY_PC_1, l);
let mut longs = [0u64; 16];
for i in 0..16 {
let ls = ARRAY_LS[i] as u32;
let mask = ARRAY_LS_MASK[ARRAY_LS[i] as usize];
// (l2 & mask) << (28 - ls) | (l2 & !mask) >> ls
let part1 = (l2 & mask).wrapping_shl(28 - ls);
let part2 = (l2 & !mask) >> ls;
l2 = part1 | part2;
longs[i] = bit_transform(&ARRAY_PC_2, l2);
}
// 解密模式:反转子密钥顺序
if mode == 1 {
for j in 0..8 {
longs.swap(j, 15 - j);
}
}
longs
}
/// DES 加密一个 64 位块
fn des64(subkeys: &[u64; 16], l: u64) -> u64 {
let mut out = bit_transform(&ARRAY_IP, l);
let mut p0 = out & 0xffffffff; // 低 32 位 (pSource[0])
let mut p1 = out >> 32; // 高 32 位 (pSource[1])
for i in 0..16 {
let mut r = p1;
r = bit_transform(&ARRAY_E, r);
r ^= subkeys[i];
// 取 8 个 8 位组
let pr: [u64; 8] = [
(r >> 0) & 255,
(r >> 8) & 255,
(r >> 16) & 255,
(r >> 24) & 255,
(r >> 32) & 255,
(r >> 40) & 255,
(r >> 48) & 255,
(r >> 56) & 255,
];
// S-Box 替换(反向遍历,每次左移 4 位)
let mut s_out: u64 = 0;
for sbi in (0..8).rev() {
s_out = (s_out << 4) | MATRIX_NS_BOX[sbi][pr[sbi] as usize] as u64;
}
r = bit_transform(&ARRAY_P, s_out);
let temp = p0;
p0 = p1;
p1 = temp ^ r;
}
// pSource.reverse() 后: pSource[0] = p1, pSource[1] = p0
// out = (pSource[1] << 32) | pSource[0] = (p0 << 32) | p1
out = (p0 << 32) | (p1 & 0xffffffff);
out = bit_transform(&ARRAY_IP_1, out);
out
}
/// 加密/解密消息
fn crypt(msg: &[u8], key: &[u8], mode: i32) -> Vec<u8> {
// 处理密钥块:将 8 字节密钥转为 u64
let mut l: u64 = 0;
for i in 0..8 {
l |= (key[i] as u64) << (i * 8);
}
let subkeys = sub_keys(l, mode);
let j = msg.len() / 8;
let mut result = Vec::new();
// 处理完整块
for m in 0..j {
let mut block: u64 = 0;
for n in 0..8 {
block |= (msg[n + m * 8] as u64) << (n * 8);
}
let encrypted = des64(&subkeys, block);
for n in 0..8 {
result.push((encrypted >> (n * 8)) as u8);
}
}
// 处理剩余字节(加密模式追加完整块)
let remainder = msg.len() % 8;
if remainder > 0 || mode == 0 {
let mut block: u64 = 0;
let start = j * 8;
for i in 0..remainder {
block |= (msg[start + i] as u64) << (i * 8);
}
let encrypted = des64(&subkeys, block);
for n in 0..8 {
result.push((encrypted >> (n * 8)) as u8);
}
}
result
}
const SECRET_KEY: &[u8] = b"ylzsxkwm";
/// 加密 query 字符串并返回 base64 编码
pub fn encrypt_query(query: &str) -> String {
use base64::Engine;
let encrypted = crypt(query.as_bytes(), SECRET_KEY, 0);
base64::engine::general_purpose::STANDARD.encode(&encrypted)
}
#[cfg(test)]
mod tests {
use super::*;
/// 验证 kwDES 加密能正常执行且输出 base64
#[test]
fn test_encrypt_query_basic() {
let query = "corp=kuwo&source=kwplayer_ar_5.1.0.0_B_jiakong_vh.apk&p2p=1&type=convert_url2&sig=0&format=mp3&rid=321260769";
let result = encrypt_query(query);
assert!(!result.is_empty());
// base64 字符串只包含合法字符
assert!(result.chars().all(|c| c.is_alphanumeric() || c == '+' || c == '/' || c == '='));
println!("kwDES encrypt_query result: {}", result);
}
/// 真实接口测试:用 kwDES 加密的 query 请求 kuwo mobi.s 获取 VIP 歌曲 URL
/// 手动运行: cargo test -- --ignored --nocapture
#[tokio::test]
#[ignore]
async fn test_kuwo_mobi_with_kwdes() {
let rid = "321260769"; // 邓紫棋「唯一」VIP 歌曲
let query = format!(
"corp=kuwo&source=kwplayer_ar_5.1.0.0_B_jiakong_vh.apk&p2p=1&type=convert_url2&sig=0&format=mp3&rid={}",
rid
);
let encrypted = encrypt_query(&query);
let url = format!("http://mobi.kuwo.cn/mobi.s?f=kuwo&q={}", encrypted);
let client = reqwest::Client::new();
let resp = client
.get(&url)
.header("user-agent", "okhttp/3.10.0")
.send()
.await
.expect("请求应成功");
let text = resp.text().await.expect("读取响应应成功");
println!("kuwo mobi.s 响应 (前 500 字符): {}", &text[..text.len().min(500)]);
// 响应中应包含 http URL
assert!(text.contains("http"), "响应应包含 URL: {}", text);
}
}