uta 0.1.2

Command-line music search and downloader for QQ Music and NetEase Cloud Music, lossless first, shipped as a single static binary. For learning and research only; non-commercial use.
//! eapi 参数加密(移植自 neteaseutils.py `EapiCryptoUtils`):
//! `md5("nobody{path}use{json}md5forencrypt")` 拼接后 AES-128-ECB + PKCS7,输出大写 hex。

use aes::Aes128;
use ecb::cipher::{BlockEncryptMut, KeyInit, block_padding::Pkcs7};
use md5::{Digest, Md5};

const EAPI_KEY: &[u8; 16] = b"e82ckenh8dichen8";
const SEP: &str = "-36cd479b6b5-";

fn hex(bytes: &[u8], upper: bool) -> String {
    bytes
        .iter()
        .map(|b| {
            if upper {
                format!("{b:02X}")
            } else {
                format!("{b:02x}")
            }
        })
        .collect()
}

/// `url` 为 `/eapi/...` 接口地址(取路径并把 `/eapi/` 换成 `/api/` 参与签名),
/// `payload` 为请求 JSON 文本(签名与加密用同一份文本)。
pub fn eapi_params(url: &str, payload: &str) -> String {
    let path = url
        .split_once("://")
        .map_or(url, |(_, rest)| rest.find('/').map_or("/", |i| &rest[i..]));
    let path = path.split(['?', '#']).next().unwrap_or(path);
    let path = path.replacen("/eapi/", "/api/", 1);
    let digest = hex(
        &Md5::digest(format!("nobody{path}use{payload}md5forencrypt")),
        false,
    );
    let text = format!("{path}{SEP}{payload}{SEP}{digest}");
    let cipher = ecb::Encryptor::<Aes128>::new(EAPI_KEY.into());
    hex(
        &cipher.encrypt_padded_vec_mut::<Pkcs7>(text.as_bytes()),
        true,
    )
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn matches_python() {
        // 期望值由 musicdl EapiCryptoUtils.encryptparams 对同一输入实际计算得出
        let payload = r#"{"ids": [1481929839], "level": "lossless", "encodeType": "flac", "header": "{\"os\": \"pc\"}"}"#;
        let got = eapi_params(
            "https://interface3.music.163.com/eapi/song/enhance/player/url/v1",
            payload,
        );
        assert_eq!(
            got,
            "FA90B329E9614F79E79598F37DC2EDB487F00D1BC4C9B24CD57E6C318B9073569338432CD7D98D1A3626E997A2C53121BFE81D9222A8D04359B4F8FFADE611200BB82A0EA8B7D5803EA22FC6D16FEC2103120DD979E9F233DBA3BF88DAFFDDDBAE669ADE4C0033AAA3F58BF0C6CB38546FA4810885F0EB19CE90ED6CAAD8CD5DC8C0A76BF64464937416FF12D9932A1D385E4B95F21F5A0EF2C6BC7D4FB3A728C00F9EA146422AF407CE03B3948E4C469EEF83350DAFEAF61AEB9849F7CE9BF3"
        );
    }

    #[test]
    fn hex_case() {
        assert_eq!(hex(&[0x0a, 0xff], true), "0AFF");
        assert_eq!(hex(&[0x0a, 0xff], false), "0aff");
    }
}