Files
MusicServer/sdk/auth/kg.ts
T
snowy-yang 354df2e739 refactor: 架构整固——分层单向化、全量 TypeScript、直接运行
- 移除 bd/mg 两平台及磁盘残留产物;清理 api-test 死脚本、musicSdk/message+options 死模块、infSign vendor
- musicSdk/ 改名 sdk/;src/common、src/auth、src/apis 整体移入 sdk/,依赖方向定为 src → sdk 严格单向
- src/ 手写 JS 全量转 .ts,删除 JS 类型垫片 modules.d.ts;源码 import 统一 .ts 后缀
- Node 原生类型剥离直接运行 .ts(engines >=22.18),取消 tsc 发射产物,tsc 仅类型检查(pnpm typecheck)
- 删除 kwdes 备用实现与 src/index.ts 转发垫片;测试移至 test/smoke-test.ts
- common/request.ts 补显式导出类型;修复 set-cookie 单值形态、ApiError 类字段等隐患

验证:tsc 零错误,smoke-test 41/41
2026-09-25 23:06:23 +08:00

104 lines
4.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 酷狗登录 —— token 绑定与验证(网关 android 签名,与 KuGouMusicApi 对齐)
// 完整的手机号/扫码登录需移植其 RSA/AES 全套协议,后续按需扩展;
// token 可从酷狗 App 抓包获取(login.token)
import crypto from 'node:crypto'
import { httpFetch } from '../common/request.ts'
import { getCredentials, setCredentials } from './store.ts'
const md5 = s => crypto.createHash('md5').update(s).digest('hex')
const SALT_KEY = '57ae12eb6890223e355ccfcb74edf70d'
const SALT_SIGN = 'OIlwieks28dk2k092lksi2UIkp'
const APPID = 1005
const CLIENTVER = 11430
const PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDIAG7QOELSYoIJvTFJhMpe1s/gbjDJX51HBNnEl5HXqTW6lQ7LC8jr9fWZTwusknp+sVGzwd40MwP6U5yDE27M/X1+UR4tvOGOqp94TJtQ1EPnWGWXngpeIW5GxoQGao1rmYWAu6oi1z9XkChrsUdC6DJE5E221wf/4WLFxwAtRQIDAQAB
-----END PUBLIC KEY-----`
const calculateMid = (guid) => BigInt(`0x${md5(guid)}`).toString()
const randomString = (n) => {
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789'
return Array.from({ length: n }, () => chars[Math.floor(Math.random() * chars.length)]).join('')
}
export const androidSign = (params, data = '') => {
const s = Object.keys(params).sort()
.map(k => `${k}=${typeof params[k] === 'object' ? JSON.stringify(params[k]) : params[k]}`)
.join('')
return md5(SALT_SIGN + s + data + SALT_SIGN)
}
/** 设备标识持久化(guid/mid/dfid 稳定复用,登录验证与取流共用同一指纹) */
export const getDevice = () => {
const dev = getCredentials('_kg_device') ?? { guid: crypto.randomUUID(), mid: '', dfid: randomString(24) }
if (!dev.mid) {
dev.mid = calculateMid(dev.guid)
setCredentials('_kg_device', dev)
}
return dev
}
/** 网关移动端签名请求(gateway.kugou.com + x-router) */
const gatewayRequest = async(url, { params: extraParams, data, router }) => {
const dev = getDevice()
const params = {
dfid: dev.dfid, mid: dev.mid, uuid: '-', appid: APPID, clientver: CLIENTVER,
clienttime: Math.floor(Date.now() / 1000),
...extraParams,
}
const signature = androidSign(params, data ? JSON.stringify(data) : '')
const qs = Object.keys(params).map(k => `${k}=${encodeURIComponent(params[k])}`).join('&')
const r = await httpFetch(`https://gateway.kugou.com${url}?${qs}&signature=${signature}`, {
method: 'post',
headers: {
dfid: dev.dfid, mid: dev.mid, clienttime: String(params.clienttime),
'kg-rc': '1', 'kg-thash': '5d816a0', 'kg-rec': '1', 'kg-rf': 'B9EDA08A64250DEFFBCADDEE00F8F25F',
'x-router': router,
'User-Agent': 'Android15-1070-11083-46-0-DiscoveryDRADProtocol-wifi',
},
body: data ?? undefined,
timeout: 15000,
}).promise
return r.body ?? {}
}
/**
* 验证 token 并返回账号资料(usercenter /v3/get_my_info)
* p = RSA 原始加密(零填充 128 字节){ token, clienttime }
*/
export const verifyToken = async({ token, userid }) => {
const unixSec = Math.floor(Date.now() / 1000)
const payload = Buffer.from(JSON.stringify({ token, clienttime: unixSec }))
const padded = Buffer.concat([Buffer.alloc(128 - payload.length), payload])
const p = crypto.publicEncrypt({ key: PUBLIC_KEY, padding: crypto.constants.RSA_NO_PADDING }, padded).toString('hex').toUpperCase()
const body = await gatewayRequest('/v3/get_my_info', {
router: 'usercenter.kugou.com',
params: { plat: 1 },
data: { visit_time: unixSec, usertype: 1, p, userid: Number(userid) || 0 },
})
if (body.status !== 1 || !body.data) {
const err = new Error(body.error ?? 'token 无效或已过期')
;(err as any).status = 401
throw err
}
return {
userid: body.data.userid ?? Number(userid),
nickname: body.data.nickname ?? body.data.username ?? null,
vip: body.data.vip ? Boolean(body.data.vip) : null,
}
}
/** 绑定并验证 kg token */
export const loginByToken = async({ token, userid }) => {
const profile = await verifyToken({ token, userid })
setCredentials('kg', { token, userid: profile.userid, nickname: profile.nickname })
return profile
}
/** 当前 kg 登录资料(未登录返回 null) */
export const status = () => {
const cred = getCredentials('kg')
return cred?.token ? { userid: cred.userid, nickname: cred.nickname ?? null } : null
}