/* 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 #include #include #include #include #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; }