feat(ncm): 网易云技术栈预研+构建验证 (agent-200) — 选型: mbedtls3.6.2裁剪(TLS+AES+MD5+RSA raw+Base64,~388KB)/jsmn(2.2KB)/qrcodegen(10.8KB)/minimp3(37.4KB), 合计~438KB; HTTPS unikey 全链路宿主+qemu-mipsel 实测通过; RSA raw 与 Python pow 逐字节一致; MP3 解码非瓶颈

vendor源码+out产物不入库(126MB, gitignore; 版本/许可/裁剪见 VENDOR_NOTES.md, build_*.sh 可复现)
This commit is contained in:
MeowOS committed 2026-08-31 06:03:36 +08:00
1 parent 2d93298f8a
commit c96e912518
19 files changed
+733

No files matched your search

+256
View File
@@ -0,0 +1,256 @@
/* https_unikey.c — 网易云 eapi 二维码 unikey 最小 HTTPS 实测 demo
* 目标端: POST https://interface.music.163.com/eapi/login/qrcode/unikey {type:3}
* 用途: 技术栈预研 — 验证 mbedtls(TLS1.2) + AES-128-ECB + MD5 + Base64(无需) + jsmn 在宿主/MIPS 均可用。
* eapi 加密(参考 CNMPlayer ncm-api-rs crypto::eapi):
* digest = md5("nobody" + url + "use" + json + "md5forencrypt") // url="/api/login/qrcode/unikey"
* plain = url + "-36cd479b6b5-" + json + "-36cd479b6b5-" + digest
* params = HEX_UPPER(AES128_ECB(plain, EAPI_KEY)) // EAPI_KEY="e82ckenh8dichen8"
* HTTP POST body = "params=" + params ; Cookie 头为 eapi header map(urlencoded 拼接)。
* 编译(宿主): gcc -O2 -Ivendor/mbedtls/include -Ivendor/jsmn demo/https_unikey.c \
* vendor/mbedtls/library/libmbedtls.a vendor/mbedtls/library/libmbedx509.a \
* vendor/mbedtls/library/libmbedcrypto.a -o demo/https_unikey
* 编译(MIPS): .tools/zig-mipsel-cc -O2 -static ... (见 build_demo.sh)
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include "mbedtls/net_sockets.h"
#include "mbedtls/ssl.h"
#include "mbedtls/x509_crt.h"
#include "mbedtls/md.h"
#include "mbedtls/aes.h"
#include "mbedtls/error.h"
#include "jsmn.h"
#define EAPI_KEY "e82ckenh8dichen8"
#define EAPI_URL "/api/login/qrcode/unikey"
#define HTTP_URL "/eapi/login/qrcode/unikey"
#define HOST "interface.music.163.com"
#define UA_EAPI "NeteaseMusic 9.0.90/5038 (iPhone; iOS 16.2; zh_CN)"
#define SEP "-36cd479b6b5-"
/* ---------- 实用: 十六进制 ---------- */
static void hex_lower(const unsigned char *in, size_t n, char *out) {
static const char d[] = "0123456789abcdef";
for (size_t i = 0; i < n; i++) { out[i*2] = d[in[i]>>4]; out[i*2+1] = d[in[i]&15]; }
out[n*2] = 0;
}
static void hex_upper(const unsigned char *in, size_t n, char *out) {
static const char d[] = "0123456789ABCDEF";
for (size_t i = 0; i < n; i++) { out[i*2] = d[in[i]>>4]; out[i*2+1] = d[in[i]&15]; }
out[n*2] = 0;
}
/* ---------- MD5 (mbedtls_md) ---------- */
static void md5_hex(const char *msg, char *out_digest_hex /*33*/) {
unsigned char d[16];
const mbedtls_md_info_t *inf = mbedtls_md_info_from_type(MBEDTLS_MD_MD5);
mbedtls_md(inf, (const unsigned char*)msg, strlen(msg), d);
hex_lower(d, 16, out_digest_hex);
}
/* ---------- AES-128-ECB + PKCS7 → 大写 Hex ---------- */
static void aes_ecb_hex(const char *plain, char *out /*hex*/) {
size_t n = strlen(plain);
int pad = 16 - (int)(n % 16);
size_t total = n + pad;
unsigned char buf[512], key[16], enc[512];
memcpy(buf, plain, n);
for (int i = 0; i < pad; i++) buf[n + i] = (unsigned char)pad;
mbedtls_aes_context aes; mbedtls_aes_init(&aes);
mbedtls_aes_setkey_enc(&aes, (const unsigned char*)EAPI_KEY, 128);
for (size_t off = 0; off < total; off += 16)
mbedtls_aes_crypt_ecb(&aes, MBEDTLS_AES_ENCRYPT, &buf[off], &enc[off]);
mbedtls_aes_free(&aes);
hex_upper(enc, total, out);
}
/* ---------- 生成 52 hex deviceId(演示用,可被环境变量覆盖) ---------- */
static void make_device_id(char *out) {
const char *env = getenv("NCM_DEVICE_ID");
if (env && strlen(env) == 52) { strcpy(out, env); return; }
unsigned char r[26]; FILE *f = fopen("/dev/urandom","rb");
if (f) { size_t got = fread(r,1,26,f); fclose(f); if (got==26){ hex_upper(r,26,out); return; } }
/* fallback 固定值 */
strcpy(out, "0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF");
}
/* ---------- 自定义 RNG (读取 /dev/urandom) ---------- */
static FILE *rng_fp = NULL;
static int my_rng(void *arg, unsigned char *out, size_t len) {
(void)arg;
if (!rng_fp) rng_fp = fopen("/dev/urandom","rb");
if (!rng_fp) return -1;
return fread(out,1,len,rng_fp) == len ? 0 : -1;
}
/* ---------- eapi 请求体构造 ---------- */
static void build_eapi(const char *device_id, char *body_out /* "params=..."; 尺寸给足 */,
char *cookie_out, size_t cookie_sz) {
char ts[32]; sprintf(ts, "%lld", (long long)time(NULL));
char ms[32]; struct timespec tv; clock_gettime(CLOCK_REALTIME,&tv);
sprintf(ms, "%lld", (long long)tv.tv_sec*1000 + tv.tv_nsec/1000000);
char buildver[16]; memcpy(buildver, ts, 10); buildver[10]=0;
char request_id[48]; sprintf(request_id, "%s_0000", ms);
/* header map (eapi header cookie & json.header 共用同一组键值) */
const char *osver = "Microsoft-Windows-10-Professional-build-19045-64bit";
const char *appver = "3.1.17.204416";
const char *os = "pc", *channel = "netease";
const char *resolution = "1920x1080", *versioncode = "140", *mobilename = "";
/* Cookie 头 (k=v 用 '; ' 拼接,值不做 urlencode,与 §7 实测一致) */
snprintf(cookie_out, cookie_sz,
"__csrf=;appver=%s;buildver=%s;channel=%s;deviceId=%s;mobilename=%s;os=%s;osver=%s;requestId=%s;resolution=%s;versioncode=%s",
appver, buildver, channel, device_id, mobilename, os, osver, request_id, resolution, versioncode);
/* json 明文 (key 顺序: type, header, e_r;header 内顺序无关紧要,服务端做 JSON 解析) */
char json[1024];
snprintf(json, sizeof json,
"{\"type\":3,\"header\":{\"__csrf\":\"\",\"appver\":\"%s\",\"buildver\":\"%s\",\"channel\":\"%s\","
"\"deviceId\":\"%s\",\"mobilename\":\"%s\",\"os\":\"%s\",\"osver\":\"%s\",\"requestId\":\"%s\","
"\"resolution\":\"%s\",\"versioncode\":\"%s\"},\"e_r\":false}",
appver, buildver, channel, device_id, mobilename, os, osver, request_id, resolution, versioncode);
/* eapi 签名 */
char msg[1400];
snprintf(msg, sizeof msg, "nobody%suse%smd5forencrypt", EAPI_URL, json);
char digest[33]; md5_hex(msg, digest);
char plain[1600];
snprintf(plain, sizeof plain, "%s%s%s%s%s", EAPI_URL, SEP, json, SEP, digest);
char params[1200];
aes_ecb_hex(plain, params);
snprintf(body_out, 1300, "params=%s", params);
}
/* ---------- jsmn 取 "unikey" 字符串值 ---------- */
static int jsmn_get_str(const char *json, const char *key, char *val, size_t vsz) {
jsmn_parser p; jsmn_init(&p);
int n = jsmn_parse(&p, json, strlen(json), NULL, 0);
if (n <= 0) return -1;
jsmntok_t *tok = malloc(sizeof(jsmntok_t)*(size_t)n);
if (!tok) return -1;
jsmn_init(&p);
if (jsmn_parse(&p, json, strlen(json), tok, n) < 0) { free(tok); return -1; }
int rc = -1;
for (int i = 0; i < n - 1; i++) {
if (tok[i].type == JSMN_STRING &&
(int)(tok[i].end - tok[i].start) == (int)strlen(key) &&
strncmp(json + tok[i].start, key, tok[i].end - tok[i].start) == 0) {
jsmntok_t *v = &tok[i+1];
if (v->type == JSMN_STRING) {
size_t len = v->end - v->start;
if (len < vsz) { memcpy(val, json + v->start, len); val[len] = 0; rc = 0; }
}
break;
}
}
free(tok); return rc;
}
/* ---------- TLS POST (mbedtls) ---------- */
static int tls_post(const char *host, const char *path, const char *cookie,
const char *body, char *resp, size_t resp_sz) {
int ret;
mbedtls_net_context net; mbedtls_net_init(&net);
mbedtls_ssl_context ssl; mbedtls_ssl_init(&ssl);
mbedtls_ssl_config conf; mbedtls_ssl_config_init(&conf);
mbedtls_x509_crt cacert; mbedtls_x509_crt_init(&cacert);
/* 载入 CA 根(宿主/system 路径; 设备侧请内置 CA bundle) */
const char *ca_paths[] = {
"/etc/ssl/certs/ca-certificates.crt",
"/etc/ssl/certs/ca-bundle.crt",
"/etc/pki/tls/certs/ca-bundle.crt",
NULL
};
int ca_ok = 0;
for (int i = 0; ca_paths[i]; i++)
if (mbedtls_x509_crt_parse_file(&cacert, ca_paths[i]) == 0) { ca_ok = 1; break; }
if (!ca_ok) { fprintf(stderr, "[!] no CA bundle loaded\n"); ret = -100; goto out; }
if ((ret = mbedtls_net_connect(&net, host, "443", MBEDTLS_NET_PROTO_TCP)) != 0) {
fprintf(stderr, "[!] connect: -0x%04x\n", (unsigned)-ret); goto out;
}
if ((ret = mbedtls_ssl_config_defaults(&conf, MBEDTLS_SSL_IS_CLIENT,
MBEDTLS_SSL_TRANSPORT_STREAM, MBEDTLS_SSL_PRESET_DEFAULT)) != 0) goto out;
mbedtls_ssl_conf_authmode(&conf, MBEDTLS_SSL_VERIFY_REQUIRED);
mbedtls_ssl_conf_ca_chain(&conf, &cacert, NULL);
mbedtls_ssl_conf_rng(&conf, my_rng, NULL);
if ((ret = mbedtls_ssl_setup(&ssl, &conf)) != 0) goto out;
if ((ret = mbedtls_ssl_set_hostname(&ssl, host)) != 0) goto out;
mbedtls_ssl_set_bio(&ssl, &net, mbedtls_net_send, mbedtls_net_recv, NULL);
while ((ret = mbedtls_ssl_handshake(&ssl)) != 0) {
if (ret != MBEDTLS_ERR_SSL_WANT_READ && ret != MBEDTLS_ERR_SSL_WANT_WRITE) {
fprintf(stderr, "[!] handshake: -0x%04x\n", (unsigned)-ret); goto out;
}
}
char req[2048];
int rlen = snprintf(req, sizeof req,
"POST %s HTTP/1.1\r\nHost: %s\r\nUser-Agent: %s\r\n"
"Content-Type: application/x-www-form-urlencoded\r\n"
"Cookie: %s\r\nConnection: close\r\nContent-Length: %zu\r\n\r\n%s",
path, host, UA_EAPI, cookie, strlen(body), body);
size_t off = 0;
while (off < (size_t)rlen) {
ret = mbedtls_ssl_write(&ssl, (const unsigned char*)req + off, (size_t)rlen - off);
if (ret < 0) {
if (ret == MBEDTLS_ERR_SSL_WANT_READ || ret == MBEDTLS_ERR_SSL_WANT_WRITE) { continue; }
fprintf(stderr, "[!] ssl_write: -0x%04x\n", (unsigned)-ret); goto out;
}
off += (size_t)ret;
}
size_t total = 0; memset(resp, 0, resp_sz);
for (;;) {
ret = mbedtls_ssl_read(&ssl, (unsigned char*)resp + total, resp_sz - 1 - total);
if (ret == 0) break; /* 对端关闭 */
if (ret < 0) {
if (ret == MBEDTLS_ERR_SSL_WANT_READ || ret == MBEDTLS_ERR_SSL_WANT_WRITE) continue;
if (ret == MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY) break;
fprintf(stderr, "[!] ssl_read: -0x%04x\n", (unsigned)-ret); goto out;
}
total += (size_t)ret;
if (total >= resp_sz - 1) break;
}
ret = (int)total;
out:
mbedtls_net_free(&net); mbedtls_ssl_free(&ssl); mbedtls_ssl_config_free(&conf);
mbedtls_x509_crt_free(&cacert);
if (rng_fp) { fclose(rng_fp); rng_fp = NULL; }
return ret;
}
int main(void) {
char device_id[64]; make_device_id(device_id);
/* 演示用随机 deviceId(实际应持久化,风控要求稳定) */
printf("deviceId=%s\n", device_id);
char body[1300], cookie[512];
build_eapi(device_id, body, cookie, sizeof cookie);
char resp[8192];
int n = tls_post(HOST, HTTP_URL, cookie, body, resp, sizeof resp);
if (n < 0) {
char eb[128]; mbedtls_strerror(n, eb, sizeof eb);
fprintf(stderr, "HTTP request failed: %d (%s)\n", n, eb);
return 1;
}
resp[n] = 0;
/* 剥掉 HTTP 头 */
char *pbody = strstr(resp, "\r\n\r\n");
pbody = pbody ? pbody + 4 : resp;
printf("---- response body ----\n%.*s\n", (int)strlen(pbody), pbody);
char unikey[64] = {0};
if (jsmn_get_str(pbody, "unikey", unikey, sizeof unikey) == 0)
printf("UNIKEY = %s\n", unikey); /* ← 拿到即成功 */
else
printf("UNIKEY not found\n");
return jsmn_get_str(pbody, "unikey", unikey, sizeof unikey) == 0 ? 0 : 2;
}