ncmapi 1.0.0

NetEase Cloud Music API for Rust.
Documentation
mod key;

use base64::{Engine as _, engine::general_purpose::STANDARD};
use openssl::{
    error::ErrorStack,
    hash::{MessageDigest, hash},
    rsa::{Padding, Rsa},
    symm::{Cipher, decrypt, encrypt},
};
use rand::RngExt;
use serde::Serialize;

use key::{BASE62, EAPI_KEY, IV, LINUX_API_KEY, PRESET_KEY, PUBLIC_KEY};

#[derive(Serialize, Debug, PartialEq, Eq, Clone, Copy)]
pub enum Crypto {
    Weapi,
    Eapi,
    #[allow(unused)]
    Linuxapi,
}

pub struct WeapiForm {
    params: String,
    enc_sec_key: String,
}

pub struct EapiForm {
    params: String,
}
pub struct LinuxapiForm {
    eparams: String,
}

impl WeapiForm {
    pub fn into_vec(self) -> Vec<(String, String)> {
        vec![
            ("params".to_owned(), self.params),
            ("encSecKey".to_owned(), self.enc_sec_key),
        ]
    }
}

impl EapiForm {
    pub fn into_vec(self) -> Vec<(String, String)> {
        vec![("params".to_owned(), self.params)]
    }
}

impl LinuxapiForm {
    pub fn into_vec(self) -> Vec<(String, String)> {
        vec![("eparams".to_owned(), self.eparams)]
    }
}

pub fn weapi(text: &[u8]) -> Result<WeapiForm, ErrorStack> {
    let mut rng = rand::rng();
    let mut rand_buf = [0u8; 16];
    rng.fill(&mut rand_buf);

    let sk = rand_buf
        .iter()
        .map(|i| BASE62.as_bytes()[(i % 62) as usize])
        .collect::<Vec<u8>>();

    let params = {
        let p = STANDARD.encode(aes_128_cbc(
            text,
            PRESET_KEY.as_bytes(),
            Some(IV.as_bytes()),
        )?);
        STANDARD.encode(aes_128_cbc(p.as_bytes(), &sk, Some(IV.as_bytes()))?)
    };

    let enc_sec_key = {
        let reversed_sk = sk.iter().rev().copied().collect::<Vec<u8>>();
        hex::encode(rsa(&reversed_sk, PUBLIC_KEY.as_bytes())?)
    };

    Ok(WeapiForm {
        params,
        enc_sec_key,
    })
}

pub fn eapi(url: &[u8], data: &[u8]) -> Result<EapiForm, ErrorStack> {
    let msg = format!(
        "nobody{}use{}md5forencrypt",
        String::from_utf8_lossy(url),
        String::from_utf8_lossy(data)
    );
    let digest = hex::encode(hash(MessageDigest::md5(), msg.as_bytes())?);

    let text = {
        let d = "-36cd479b6b5-";
        [url, d.as_bytes(), data, d.as_bytes(), digest.as_bytes()].concat()
    };

    let params = {
        let p = aes_128_ecb(&text, EAPI_KEY.as_bytes(), None)?;
        hex::encode_upper(p)
    };

    Ok(EapiForm { params })
}

#[allow(unused)]
pub fn eapi_decrypt(ct: &[u8]) -> Result<Vec<u8>, ErrorStack> {
    aes_128_ecb_decrypt(ct, EAPI_KEY.as_bytes(), None)
}

pub fn linuxapi(text: &[u8]) -> Result<LinuxapiForm, ErrorStack> {
    let ct = aes_128_ecb(text, LINUX_API_KEY.as_bytes(), None)?;
    let eparams = hex::encode_upper(ct);

    Ok(LinuxapiForm { eparams })
}

fn aes_128_ecb(pt: &[u8], key: &[u8], iv: Option<&[u8]>) -> Result<Vec<u8>, ErrorStack> {
    let cipher = Cipher::aes_128_ecb();
    encrypt(cipher, key, iv, pt)
}

fn aes_128_ecb_decrypt(ct: &[u8], key: &[u8], iv: Option<&[u8]>) -> Result<Vec<u8>, ErrorStack> {
    let cipher = Cipher::aes_128_ecb();
    decrypt(cipher, key, iv, ct)
}

fn aes_128_cbc(pt: &[u8], key: &[u8], iv: Option<&[u8]>) -> Result<Vec<u8>, ErrorStack> {
    let cipher = Cipher::aes_128_cbc();
    encrypt(cipher, key, iv, pt)
}

fn rsa(pt: &[u8], key: &[u8]) -> Result<Vec<u8>, ErrorStack> {
    let rsa = Rsa::public_key_from_pem(key)?;

    let prefix = vec![0u8; 128 - pt.len()];
    let pt = [&prefix[..], pt].concat();

    let mut ct = vec![0; rsa.size() as usize];
    rsa.public_encrypt(&pt, &mut ct, Padding::NONE)?;
    Ok(ct)
}

#[cfg(test)]
mod tests {
    use super::key::{EAPI_KEY, IV, PRESET_KEY, PUBLIC_KEY};
    use super::{aes_128_cbc, aes_128_ecb, aes_128_ecb_decrypt, rsa, weapi};
    use crate::crypto::{eapi, eapi_decrypt, linuxapi};

    #[test]
    fn test_aes_128_ecb() {
        let pt = "plain text";
        let ct = aes_128_ecb(pt.as_bytes(), EAPI_KEY.as_bytes(), None).expect("valid test cipher");
        let _pt = aes_128_ecb_decrypt(&ct, EAPI_KEY.as_bytes(), None);
        assert!(_pt.is_ok());

        if let Ok(decrypted) = _pt {
            assert_eq!(&decrypted, pt.as_bytes());
        }
    }

    #[test]
    fn test_aes_cbc() {
        let pt = "plain text";
        let ct = aes_128_cbc(pt.as_bytes(), PRESET_KEY.as_bytes(), Some(IV.as_bytes()))
            .expect("valid test cipher");
        assert!(hex::encode(ct).ends_with("baf0"))
    }

    #[test]
    fn test_rsa() {
        let ct = rsa(PRESET_KEY.as_bytes(), PUBLIC_KEY.as_bytes()).expect("valid public key");
        assert!(hex::encode(ct).ends_with("4413"));
    }

    #[test]
    fn test_weapi() {
        weapi(r#"{"username": "alex"}"#.as_bytes()).expect("valid weapi payload");
    }

    #[test]
    fn test_eapi() {
        let ct = eapi("/url".as_bytes(), "plain text".as_bytes()).expect("valid eapi payload");
        assert!(ct.params.ends_with("C3F3"));
    }

    #[test]
    fn test_eapi_decrypt() {
        let pt = "plain text";
        let ct = aes_128_ecb(pt.as_bytes(), EAPI_KEY.as_bytes(), None).expect("valid test cipher");
        assert_eq!(pt.as_bytes(), &eapi_decrypt(&ct).unwrap())
    }

    #[test]
    fn test_linuxapi() {
        let ct = linuxapi(r#""plain text""#.as_bytes()).expect("valid linuxapi payload");
        assert!(ct.eparams.ends_with("2250"));
    }
}