feat(qs): 汽水音乐平台接入——匿名搜索/试听、抖音扫码登录、解密流代理

- sdk/qs 平台模块(安卓参数指纹匿名搜索)+ sdk/apis/qs 取流:
  匿名 30s 试听,登录态走签名 track_v2 + spade key AES-CTR 解密
- /login/qs/qr 抖音扫码登录(授权后 sessionid 解锁完整曲目)
- /stream/qs/:id 解密流代理,/url 返回的相对地址按请求 host 补全
- 回归测试覆盖 kw 双通道(VIP 真音频/降级链)与 qs 试听;start.bat 去掉
  SOURCE_ENABLED 直启
This commit is contained in:
snowy-yang committed 2026-10-01 22:43:49 +08:00
1 parent aa4f2eec67
commit 0f9cff2a08
26 files changed
+2172 -9

No files matched your search

+152
View File
@@ -0,0 +1,152 @@
import { b64encode, getCrypto, getSubtle, hash, pkcs7Pad } from "./index.ts";
import { ProtobufWriter } from "./protobuf.ts";
import { simonEncrypt } from "./simon.ts";
import { sm3 } from "./sm3.ts";
import type { UnidbgConfig } from "./unidbgconfig.ts";
const LOW_RAND = new Uint8Array([0xf2, 0x81]);
const HIGH_RAND = new Uint8Array([0x61, 0x6f]); // 'a', 'o'
const XOR_PREFIX = new Uint8Array([0xf2, 0xf7, 0xfc, 0xff, 0xf2, 0xf7, 0xfc, 0xff]);
function sm3Prefix6(data: Uint8Array): Uint8Array<ArrayBuffer> {
return sm3(data).slice(0, 6);
}
function decodeStub(xssStub: string): Uint8Array<ArrayBuffer> {
const bytes = new Uint8Array(16);
if (xssStub.length >= 32) {
for (let i = 0; i < 16; i++) {
bytes[i] = parseInt(xssStub.slice(i * 2, i * 2 + 2), 16) & 0xff;
}
}
return bytes;
}
function calculateConstant(code: string): number {
const parts = [...code.replace(/\./g, "").padStart(6, "0")].map(Number);
const weights = [20480, 2048, 20971520, 2097152, 1342177280, 134217728];
return parts.reduce((sum, part, i) => sum + part * weights[i]!, 0);
}
function buildProtobuf(
query: string,
xssStub: string,
timestamp: number,
config: UnidbgConfig,
): Uint8Array<ArrayBuffer> {
const params = new URLSearchParams(query);
const deviceId = params.get("device_id") ?? "";
const versionName = params.get("version_name") ?? "";
const deviceType = params.get("device_type") ?? "";
const osVersion = params.get("os_version") ?? "";
const channel = params.get("channel") ?? "";
// 空 stub / 空 query 时都对 16 字节全零求 SM3
const bodyHash = sm3Prefix6(xssStub === "" ? new Uint8Array(16) : decodeStub(xssStub));
const queryHash = sm3Prefix6(
query === "" ? new Uint8Array(16) : new TextEncoder().encode(query),
);
const rand = getCrypto().getRandomValues(new Uint32Array(1))[0]! % 0x7fffffff;
return new ProtobufWriter()
.varint(1, 0x20200929 * 2) // magic
.varint(2, 2) // version
.varint(3, rand)
.string(4, config.aid)
.string(5, deviceId)
.string(6, config.licenseId)
.string(7, versionName)
.string(8, config.sdkVersion)
.varint(9, config.sdkVersionInt)
.bytes(10, new Uint8Array(8)) // envcode
.varint(11, 0) // platform
.varint(12, timestamp * 2) // createTime
.bytes(13, bodyHash)
.bytes(14, queryHash)
.message(15, (sub) => {
sub.varint(1, 1).varint(2, 1).varint(3, 1).varint(7, 3348294860);
})
.string(16, config.secDeviceId ?? "") // sec_device_id
.string(20, "none") // pskVersion
.varint(21, config.callType)
.message(23, (sub) => {
sub.string(1, deviceType).string(2, osVersion).string(3, channel).varint(4, calculateConstant(osVersion));
})
.varint(25, 2)
.toBytes();
}
export async function getArgus(
query: string,
xssStub: string,
timestamp: number,
config: UnidbgConfig,
): Promise<string> {
const { signKey } = config;
if (signKey.length !== 32) {
throw new Error(`Sign key must be 32 bytes, got ${signKey.length}`);
}
const protobuf = pkcs7Pad(buildProtobuf(query, xssStub, timestamp, config), 16);
const sm3Input = new Uint8Array(signKey.length * 2 + LOW_RAND.length + HIGH_RAND.length);
sm3Input.set(signKey, 0);
sm3Input.set(LOW_RAND, signKey.length);
sm3Input.set(HIGH_RAND, signKey.length + LOW_RAND.length);
sm3Input.set(signKey, signKey.length + LOW_RAND.length + HIGH_RAND.length);
const sm3Output = sm3(sm3Input);
const keyView = new DataView(sm3Output.buffer, sm3Output.byteOffset, sm3Output.byteLength);
const simonKey: [bigint, bigint, bigint, bigint] = [
keyView.getBigUint64(0, true),
keyView.getBigUint64(8, true),
keyView.getBigUint64(16, true),
keyView.getBigUint64(24, true),
];
const encrypted = new Uint8Array(protobuf.length);
const pbView = new DataView(protobuf.buffer, protobuf.byteOffset, protobuf.byteLength);
const encView = new DataView(encrypted.buffer);
for (let offset = 0; offset < protobuf.length; offset += 16) {
const [low, high] = simonEncrypt(
[pbView.getBigUint64(offset, true), pbView.getBigUint64(offset + 8, true)],
simonKey,
);
encView.setBigUint64(offset, low, true);
encView.setBigUint64(offset + 8, high, true);
}
const data = new Uint8Array(XOR_PREFIX.length + encrypted.length);
data.set(XOR_PREFIX);
data.set(encrypted, XOR_PREFIX.length);
for (let i = XOR_PREFIX.length; i < data.length; i++) {
data[i]! ^= data[i % 8]!;
}
data.reverse();
const header = new Uint8Array([0xa6, 0x6e, 0xad, 0x9f, 0x77, 0x01, 0xd0, 0x0c, 0x18]);
const plaintext = new Uint8Array(header.length + data.length + HIGH_RAND.length);
plaintext.set(header);
plaintext.set(data, header.length);
plaintext.set(HIGH_RAND, header.length + data.length);
const subtle = getSubtle();
const aesKey = await subtle.importKey(
"raw",
await hash.md5bytes(signKey.slice(0, 16)),
{ name: "AES-CBC" },
false,
["encrypt"],
);
const iv = await hash.md5bytes(signKey.slice(16));
const ciphertext = new Uint8Array(
await subtle.encrypt({ name: "AES-CBC", iv }, aesKey, plaintext),
);
const result = new Uint8Array(LOW_RAND.length + ciphertext.length);
result.set(LOW_RAND);
result.set(ciphertext, LOW_RAND.length);
return b64encode(result.buffer);
}
+385
View File
@@ -0,0 +1,385 @@
// CENC 音频解密(AES-CTR 逐样本)—— 移植自 guowenye/qishui-api src/audioDecryptor.js
// spade_a/PlayAuth → 32 位 hex key;密音频(m4a CENC / flac)→ 明文音频 Buffer
import { createDecipheriv } from 'node:crypto'
const encryptedBoxTypes = new Set(['senc', 'saio', 'saiz', 'sinf', 'schi', 'tenc', 'schm', 'frma'])
const containerBoxTypes = new Set(['moov', 'trak', 'mdia', 'minf', 'stbl', 'stsd'])
const maxSampleCount = 200000
const maxChunkCount = 200000
const maxStscEntryCount = 20000
function decodeBase36(codePoint) {
if (codePoint >= 48 && codePoint <= 57) return codePoint - 48
if (codePoint >= 97 && codePoint <= 122) return codePoint - 97 + 10
return 0xFF
}
function bitCount(number) {
let value = number - ((number >> 1) & 0x55555555)
value = (value & 0x33333333) + ((value >> 2) & 0x33333333)
return (((value + (value >> 4)) & 0xF0F0F0F) * 0x1010101) >> 24
}
function decryptSpadeInner(spadeKey) {
const result = new Uint8Array(spadeKey)
const buffer = new Uint8Array(2 + spadeKey.length)
buffer.set([0xFA, 0x55], 0)
buffer.set(spadeKey, 2)
for (let index = 0; index < result.length; index += 1) {
let value = (spadeKey[index] ^ buffer[index]) - bitCount(index) - 21
while (value < 0) value += 0xFF
result[index] = value
}
return result
}
function decryptSpade(spadeKeyBytes) {
const spadeKeyLength = spadeKeyBytes.length
if (spadeKeyLength < 3) return ''
const paddingLength = (spadeKeyBytes[0] ^ spadeKeyBytes[1] ^ spadeKeyBytes[2]) - 48
if (spadeKeyLength < paddingLength + 2) return ''
const innerInput = spadeKeyBytes.slice(1, spadeKeyLength - paddingLength)
const decodedBuffer = decryptSpadeInner(innerInput)
if (decodedBuffer.length === 0) return ''
const skipBytes = decodeBase36(decodedBuffer[0])
const decodedMessageLength = spadeKeyLength - paddingLength - 2
const endIndex = 1 + decodedMessageLength - skipBytes
if (endIndex > decodedBuffer.length) return ''
return Buffer.from(decodedBuffer.slice(1, endIndex)).toString('utf8')
}
function decryptSpadeA(spadeA) {
try {
return decryptSpade(new Uint8Array(Buffer.from(String(spadeA || '').trim(), 'base64')))
} catch (_) {
return ''
}
}
function isHexKey(value) {
return typeof value === 'string' && /^[a-f0-9]{32}$/i.test(value.trim())
}
function resolveAudioKey(source: any = {}) {
const rawHexKey = source.hex_key || source.key
if (isHexKey(rawHexKey)) return rawHexKey.trim().toLowerCase()
const rawSpadeA = source.spade_a || source.spadeA
if (!rawSpadeA) throw new Error('spade_a 或 hex_key 不能为空')
const hexKey = decryptSpadeA(rawSpadeA).trim()
if (!isHexKey(hexKey)) throw new Error('spade_a 或 hex_key 无效')
return hexKey.toLowerCase()
}
function hexToBytes(hex) {
if (!isHexKey(hex)) throw new Error('hex_key 必须是 32 位十六进制 AES-128 密钥')
return Buffer.from(hex, 'hex')
}
function asUint8Array(input) {
if (input instanceof Uint8Array) return input
if (Buffer.isBuffer(input)) return new Uint8Array(input.buffer, input.byteOffset, input.byteLength)
if (input instanceof ArrayBuffer) return new Uint8Array(input)
throw new Error('音频数据必须是 Buffer、Uint8Array 或 ArrayBuffer')
}
function concatUint8Arrays(arrays: any[]) {
const totalLength = arrays.reduce((sum, array) => sum + array.length, 0)
const result = new Uint8Array(totalLength)
let offset = 0
for (const array of arrays) {
result.set(array, offset)
offset += array.length
}
return result
}
function readUint32BE(fileData, offset) {
if (!Number.isInteger(offset) || offset < 0 || offset + 4 > fileData.length) {
throw new Error('音频容器结构无效')
}
return new DataView(fileData.buffer, fileData.byteOffset, fileData.byteLength).getUint32(offset, false)
}
function writeUint32BE(value) {
const output = new Uint8Array(4)
new DataView(output.buffer).setUint32(0, value, false)
return output
}
function readAscii(fileData, start, end) {
return Buffer.from(fileData.subarray(start, end)).toString('ascii')
}
class MP4Box {
offset: number
size: number
type: string
data: Uint8Array
constructor(fileData: Uint8Array, offset: number) {
this.offset = offset
this.size = readUint32BE(fileData, offset)
this.type = readAscii(fileData, offset + 4, offset + 8)
this.data = fileData.subarray(offset + 8, offset + this.size)
}
static findBox(fileData: Uint8Array, boxType: string, offset = 0, end: number | null = null) {
const searchEnd = end === null ? fileData.length : end
let position = offset
while (position < searchEnd) {
if (position + 8 > searchEnd) break
const boxSize = readUint32BE(fileData, position)
if (boxSize === 0 || boxSize > searchEnd - position || boxSize < 8) break
const currentType = readAscii(fileData, position + 4, position + 8)
if (currentType === boxType) return new MP4Box(fileData, position)
position += boxSize
}
return null
}
}
function requireBox(box, name) {
if (!box) throw new Error(`音频容器缺少 ${name} box`)
return box
}
function parseStsz(stszData) {
const sampleSize = readUint32BE(stszData, 4)
const sampleCount = readUint32BE(stszData, 8)
if (sampleCount > maxSampleCount) throw new Error('音频样本数量超过限制')
if (sampleSize !== 0) return new Array(sampleCount).fill(sampleSize)
const requiredLength = 12 + sampleCount * 4
if (requiredLength > stszData.length) throw new Error('stsz box 样本大小数据不完整')
const sampleSizes: number[] = []
for (let index = 0; index < sampleCount; index += 1) {
sampleSizes.push(readUint32BE(stszData, 12 + index * 4))
}
return sampleSizes
}
function parseStsc(stscData) {
const entryCount = readUint32BE(stscData, 4)
if (entryCount > maxStscEntryCount) throw new Error('stsc entry 数量超过限制')
const requiredLength = 8 + entryCount * 12
if (requiredLength > stscData.length) throw new Error('stsc box 数据不完整')
const entries: any[] = []
for (let index = 0; index < entryCount; index += 1) {
const baseOffset = 8 + index * 12
entries.push({
firstChunk: readUint32BE(stscData, baseOffset),
samplesPerChunk: readUint32BE(stscData, baseOffset + 4),
id: readUint32BE(stscData, baseOffset + 8),
})
}
return entries
}
function parseSenc(sencData) {
const sampleCount = readUint32BE(sencData, 4)
if (sampleCount > maxSampleCount) throw new Error('senc 样本数量超过限制')
const requiredLength = 8 + sampleCount * 8
if (requiredLength > sencData.length) throw new Error('senc box IV 数据不完整')
const ivs: Uint8Array[] = []
let position = 8
for (let index = 0; index < sampleCount; index += 1) {
const iv = new Uint8Array(16)
iv.set(sencData.subarray(position, position + 8))
ivs.push(iv)
position += 8
}
return ivs
}
function calculateChunkOffsets(sampleSizes, stscEntries, chunkCount, baseOffset) {
const offsets: number[] = []
let currentOffset = baseOffset
let sampleIndex = 0
for (let chunkIndex = 1; chunkIndex <= chunkCount; chunkIndex += 1) {
offsets.push(currentOffset)
let samplesPerChunk = 0
for (let entryIndex = 0; entryIndex < stscEntries.length; entryIndex += 1) {
const currentEntry = stscEntries[entryIndex]
const nextEntry = stscEntries[entryIndex + 1]
if (chunkIndex >= currentEntry.firstChunk && (!nextEntry || chunkIndex < nextEntry.firstChunk)) {
samplesPerChunk = currentEntry.samplesPerChunk
break
}
}
for (let sampleOffset = 0; sampleOffset < samplesPerChunk; sampleOffset += 1) {
if (sampleIndex < sampleSizes.length) currentOffset += sampleSizes[sampleIndex]
sampleIndex += 1
}
}
return offsets
}
function updateStco(stcoData, offsets) {
const chunkCount = readUint32BE(stcoData, 4)
if (offsets.length < chunkCount) throw new Error('stco 偏移数量不足')
const header = stcoData.subarray(0, 8)
const body = new Uint8Array(chunkCount * 4)
const view = new DataView(body.buffer)
for (let index = 0; index < chunkCount; index += 1) view.setUint32(index * 4, offsets[index], false)
return concatUint8Arrays([header, body])
}
function processBoxTree(fileData, offset, size, newMdatOffset, context) {
const parts: Uint8Array[] = []
let position = offset + 8
const end = offset + size
while (position < end) {
if (position + 8 > end) {
parts.push(fileData.subarray(position, end))
break
}
const boxSize = readUint32BE(fileData, position)
if (boxSize < 8 || boxSize > end - position) {
parts.push(fileData.subarray(position, end))
break
}
const type = readAscii(fileData, position + 4, position + 8)
if (encryptedBoxTypes.has(type)) {
position += boxSize
continue
}
if (type === 'enca') {
const inner = processBoxTree(fileData, position, boxSize, newMdatOffset, context)
parts.push(writeUint32BE(inner.length + 8), Buffer.from('mp4a'), inner)
position += boxSize
continue
}
if (type === 'stco') {
const offsets = calculateChunkOffsets(context.sampleSizes, context.stscEntries, context.chunkCount, newMdatOffset)
const updatedBody = updateStco(fileData.subarray(position + 8, position + boxSize), offsets)
parts.push(writeUint32BE(updatedBody.length + 8), Buffer.from('stco'), updatedBody)
position += boxSize
continue
}
if (containerBoxTypes.has(type)) {
const inner = processBoxTree(fileData, position, boxSize, newMdatOffset, context)
parts.push(writeUint32BE(inner.length + 8), Buffer.from(type), inner)
position += boxSize
continue
}
parts.push(fileData.subarray(position, position + boxSize))
position += boxSize
}
return concatUint8Arrays(parts)
}
function scanForFlacMetadata(stsdData) {
const marker = Buffer.from('dfLa')
for (let index = 4; index < stsdData.length - 4; index += 1) {
if (
stsdData[index] === marker[0]
&& stsdData[index + 1] === marker[1]
&& stsdData[index + 2] === marker[2]
&& stsdData[index + 3] === marker[3]
) {
const boxSize = readUint32BE(stsdData, index - 4)
if (boxSize >= 8 && index - 4 + boxSize <= stsdData.length) {
return stsdData.subarray(index + 4, index - 4 + boxSize)
}
}
}
return null
}
function decryptCtr(encryptedSample, keyBytes, iv) {
const decipher = createDecipheriv(`aes-${keyBytes.length * 8}-ctr`, keyBytes, Buffer.from(iv))
decipher.setAutoPadding(false)
return Buffer.concat([decipher.update(Buffer.from(encryptedSample)), decipher.final()])
}
function decryptAudioBuffer(input, hexKey) {
const fileData = asUint8Array(input)
const keyBytes = hexToBytes(hexKey)
const ftyp = MP4Box.findBox(fileData, 'ftyp')
const moov = requireBox(MP4Box.findBox(fileData, 'moov'), 'moov')
const trak = requireBox(MP4Box.findBox(fileData, 'trak', moov.offset + 8, moov.offset + moov.size), 'trak')
const mdia = requireBox(MP4Box.findBox(fileData, 'mdia', trak.offset + 8, trak.offset + trak.size), 'mdia')
const minf = requireBox(MP4Box.findBox(fileData, 'minf', mdia.offset + 8, mdia.offset + mdia.size), 'minf')
const stbl = requireBox(MP4Box.findBox(fileData, 'stbl', minf.offset + 8, minf.offset + minf.size), 'stbl')
const stsd = requireBox(MP4Box.findBox(fileData, 'stsd', stbl.offset + 8, stbl.offset + stbl.size), 'stsd')
const stsz = requireBox(MP4Box.findBox(fileData, 'stsz', stbl.offset + 8, stbl.offset + stbl.size), 'stsz')
const stsc = requireBox(MP4Box.findBox(fileData, 'stsc', stbl.offset + 8, stbl.offset + stbl.size), 'stsc')
const stco = requireBox(MP4Box.findBox(fileData, 'stco', stbl.offset + 8, stbl.offset + stbl.size), 'stco')
const mdat = requireBox(MP4Box.findBox(fileData, 'mdat'), 'mdat')
let senc = MP4Box.findBox(fileData, 'senc', stbl.offset + 8, stbl.offset + stbl.size)
if (!senc) senc = MP4Box.findBox(fileData, 'senc', moov.offset + 8, moov.offset + moov.size)
requireBox(senc, 'senc')
const sampleSizes = parseStsz(stsz.data)
const stscEntries = parseStsc(stsc.data)
const chunkCount = readUint32BE(stco.data, 4)
if (chunkCount > maxChunkCount) throw new Error('音频 chunk 数量超过限制')
if (8 + chunkCount * 4 > stco.data.length) throw new Error('stco box 数据不完整')
const ivs = parseSenc(requireBox(senc, 'senc').data)
if (ivs.length < sampleSizes.length) throw new Error('senc IV 数量少于样本数量')
const encryptedPayloadSize = sampleSizes.reduce((sum, sampleSize) => sum + sampleSize, 0)
if (encryptedPayloadSize > mdat.size - 8) throw new Error('样本总大小超过 mdat 数据范围')
const decryptedSamples: Uint8Array[] = []
let sampleOffset = mdat.offset + 8
for (let index = 0; index < sampleSizes.length; index += 1) {
const sampleSize = sampleSizes[index]
if (sampleOffset + sampleSize > fileData.length) throw new Error('音频样本数据不完整')
decryptedSamples.push(decryptCtr(fileData.subarray(sampleOffset, sampleOffset + sampleSize), keyBytes, ivs[index]))
sampleOffset += sampleSize
}
const flacMetadata = scanForFlacMetadata(stsd.data)
if (flacMetadata) {
const metadataStart = flacMetadata.length > 4 ? 4 : 0
const outputBuffer = Buffer.from(concatUint8Arrays([Buffer.from('fLaC'), flacMetadata.subarray(metadataStart), ...decryptedSamples]))
return {
buffer: outputBuffer,
extension: '.flac',
content_type: 'audio/flac',
codec: 'flac',
sample_count: sampleSizes.length,
}
}
const context = { sampleSizes, stscEntries, chunkCount }
const ftypSize = ftyp ? ftyp.size : 0
const dummyMoov = processBoxTree(fileData, moov.offset, moov.size, 0, context)
const newMdatOffset = ftypSize + dummyMoov.length + 16
const cleanMoovData = processBoxTree(fileData, moov.offset, moov.size, newMdatOffset, context)
const cleanMoov = concatUint8Arrays([writeUint32BE(cleanMoovData.length + 8), Buffer.from('moov'), cleanMoovData])
const mdatData = concatUint8Arrays(decryptedSamples)
const newMdat = concatUint8Arrays([writeUint32BE(mdatData.length + 8), Buffer.from('mdat'), mdatData])
const finalParts: Uint8Array[] = []
if (ftyp) finalParts.push(fileData.subarray(ftyp.offset, ftyp.offset + ftyp.size))
finalParts.push(cleanMoov, newMdat)
return {
buffer: Buffer.from(concatUint8Arrays(finalParts)),
extension: '.m4a',
content_type: 'audio/mp4',
codec: 'mp4',
sample_count: sampleSizes.length,
}
}
export {
decryptSpadeA,
isHexKey,
resolveAudioKey,
decryptAudioBuffer,
scanForFlacMetadata,
MP4Box,
}
+120
View File
@@ -0,0 +1,120 @@
import md5 from "./md5.ts";
import { sm3 } from "./sm3.ts";
export function getCrypto(): Crypto {
const c = globalThis.crypto as Crypto | undefined;
if (!c?.subtle) {
throw new Error("Crypto API不可用,请检查浏览器版本是否支持该API");
}
return c;
}
export function getSubtle(): SubtleCrypto {
return getCrypto().subtle;
}
export function b64decode(b64: string): ArrayBuffer {
const binaryString = atob(b64);
const len = binaryString.length;
const bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes.buffer;
}
export function b64encode(buffer: ArrayBuffer): string {
const bytes = new Uint8Array(buffer);
const chunkSize = 0x8000;
const chunks: string[] = [];
for (let i = 0; i < bytes.length; i += chunkSize) {
chunks.push(
String.fromCharCode(...bytes.subarray(i, i + chunkSize)),
);
}
return btoa(chunks.join(""));
}
export function unhex(hex: string): ArrayBuffer {
if (hex.length % 2 !== 0) {
throw new Error("Invalid hex string");
}
const bytes = new Uint8Array(hex.length / 2);
for (let i = 0; i < hex.length; i += 2) {
const byte = parseInt(hex.slice(i, i + 2), 16);
if (Number.isNaN(byte)) {
throw new Error("Invalid hex string");
}
bytes[i / 2] = byte;
}
return bytes.buffer;
}
export function hex(buffer: ArrayBuffer): string {
const bytes = new Uint8Array(buffer);
let hexString = "";
for (let i = 0; i < bytes.length; i++) {
hexString += bytes[i]!.toString(16).padStart(2, "0");
}
return hexString;
}
export function pkcs7Pad(data: Uint8Array, blockSize = 16): Uint8Array<ArrayBuffer> {
const padLength = blockSize - (data.length % blockSize);
const padded = new Uint8Array(data.length + padLength);
padded.set(data);
padded.fill(padLength, data.length);
return padded;
}
export function randomString(length: number): string {
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let result = "";
const array = new Uint8Array(length);
getCrypto().getRandomValues(array);
for (let i = 0; i < length; i++) {
result += chars.charAt(array[i]! % chars.length);
}
return result;
}
type HashInput = ArrayBuffer | Uint8Array<ArrayBuffer> | string;
function toBytes(input: HashInput): Uint8Array<ArrayBuffer> {
if (typeof input === "string") {
return new TextEncoder().encode(input);
}
if (input instanceof Uint8Array) {
return new Uint8Array(input);
}
return new Uint8Array(input);
}
export const hash = {
sha256: async (input: HashInput): Promise<string> => {
const subtle = getSubtle();
const digest = await subtle.digest("SHA-256", toBytes(input));
return hex(digest);
},
sha256bytes: async (input: HashInput): Promise<ArrayBuffer> => {
const subtle = getSubtle();
return subtle.digest("SHA-256", toBytes(input));
},
sha512: async (input: HashInput): Promise<string> => {
const subtle = getSubtle();
const digest = await subtle.digest("SHA-512", toBytes(input));
return hex(digest);
},
sha512bytes: async (input: HashInput): Promise<ArrayBuffer> => {
const subtle = getSubtle();
return subtle.digest("SHA-512", toBytes(input));
},
md5: async (input: HashInput): Promise<string> => md5(
typeof input === "string" ? input : toBytes(input).buffer,
),
md5bytes: async (input: HashInput): Promise<ArrayBuffer> => unhex(md5(
typeof input === "string" ? input : toBytes(input).buffer,
)),
sm3: async (input: HashInput): Promise<string> => hex(sm3(toBytes(input)).buffer),
sm3bytes: async (input: HashInput): Promise<ArrayBuffer> => sm3(toBytes(input)).buffer,
};
+18
View File
@@ -0,0 +1,18 @@
import { b64encode, getCrypto } from "./index.ts";
import { generateLadonKey, speckEncrypt } from "./speck.ts";
import type { UnidbgConfig } from "./unidbgconfig.ts";
export async function getLadon(timestamp: number, config: UnidbgConfig): Promise<string> {
const randomBytes = getCrypto().getRandomValues(new Uint8Array(4));
const plaintext = new TextEncoder().encode(
`${timestamp}-${config.licenseId}-${config.aid}`,
);
const key = await generateLadonKey(randomBytes, config.aid);
const encrypted = speckEncrypt(key, plaintext);
const result = new Uint8Array(randomBytes.length + encrypted.length);
result.set(randomBytes);
result.set(encrypted, randomBytes.length);
return b64encode(result.buffer);
}
+161
View File
@@ -0,0 +1,161 @@
function rotateLeft(value: number, shiftBits: number): number {
return (value << shiftBits) | (value >>> (32 - shiftBits));
}
function addUnsigned(left: number, right: number): number {
return (left + right) >>> 0;
}
function f(x: number, y: number, z: number): number {
return (x & y) | (~x & z);
}
function g(x: number, y: number, z: number): number {
return (x & z) | (y & ~z);
}
function h(x: number, y: number, z: number): number {
return x ^ y ^ z;
}
function i(x: number, y: number, z: number): number {
return y ^ (x | ~z);
}
function ff(a: number, b: number, c: number, d: number, x: number, s: number, ac: number): number {
return addUnsigned(rotateLeft(addUnsigned(a, addUnsigned(addUnsigned(f(b, c, d), x), ac)), s), b);
}
function gg(a: number, b: number, c: number, d: number, x: number, s: number, ac: number): number {
return addUnsigned(rotateLeft(addUnsigned(a, addUnsigned(addUnsigned(g(b, c, d), x), ac)), s), b);
}
function hh(a: number, b: number, c: number, d: number, x: number, s: number, ac: number): number {
return addUnsigned(rotateLeft(addUnsigned(a, addUnsigned(addUnsigned(h(b, c, d), x), ac)), s), b);
}
function ii(a: number, b: number, c: number, d: number, x: number, s: number, ac: number): number {
return addUnsigned(rotateLeft(addUnsigned(a, addUnsigned(addUnsigned(i(b, c, d), x), ac)), s), b);
}
function convertToWordArray(bytes: Uint8Array): number[] {
const wordCount = Math.ceil((bytes.length + 9) / 64) * 16;
const words = new Array<number>(wordCount).fill(0);
for (let byteIndex = 0; byteIndex < bytes.length; byteIndex++) {
const wordIndex = Math.floor(byteIndex / 4);
const bytePosition = (byteIndex % 4) * 8;
words[wordIndex] = words[wordIndex]! | (bytes[byteIndex]! << bytePosition);
}
const paddingWordIndex = Math.floor(bytes.length / 4);
const paddingBytePosition = (bytes.length % 4) * 8;
words[paddingWordIndex] = words[paddingWordIndex]! | (0x80 << paddingBytePosition);
words[wordCount - 2] = bytes.length << 3;
words[wordCount - 1] = bytes.length >>> 29;
return words;
}
function wordToHex(value: number): string {
let hex = "";
for (let byteIndex = 0; byteIndex < 4; byteIndex++) {
const byte = (value >>> (byteIndex * 8)) & 0xff;
hex += byte.toString(16).padStart(2, "0");
}
return hex;
}
/**
* 计算 MD5 摘要。
*/
export default function md5(input: string | ArrayBuffer): string {
const bytes = typeof input === "string" ? new TextEncoder().encode(input) : new Uint8Array(input);
const words = convertToWordArray(bytes);
let a = 0x67452301;
let b = 0xefcdab89;
let c = 0x98badcfe;
let d = 0x10325476;
for (let k = 0; k < words.length; k += 16) {
const aa = a;
const bb = b;
const cc = c;
const dd = d;
a = ff(a, b, c, d, words[k]!, 7, 0xd76aa478);
d = ff(d, a, b, c, words[k + 1]!, 12, 0xe8c7b756);
c = ff(c, d, a, b, words[k + 2]!, 17, 0x242070db);
b = ff(b, c, d, a, words[k + 3]!, 22, 0xc1bdceee);
a = ff(a, b, c, d, words[k + 4]!, 7, 0xf57c0faf);
d = ff(d, a, b, c, words[k + 5]!, 12, 0x4787c62a);
c = ff(c, d, a, b, words[k + 6]!, 17, 0xa8304613);
b = ff(b, c, d, a, words[k + 7]!, 22, 0xfd469501);
a = ff(a, b, c, d, words[k + 8]!, 7, 0x698098d8);
d = ff(d, a, b, c, words[k + 9]!, 12, 0x8b44f7af);
c = ff(c, d, a, b, words[k + 10]!, 17, 0xffff5bb1);
b = ff(b, c, d, a, words[k + 11]!, 22, 0x895cd7be);
a = ff(a, b, c, d, words[k + 12]!, 7, 0x6b901122);
d = ff(d, a, b, c, words[k + 13]!, 12, 0xfd987193);
c = ff(c, d, a, b, words[k + 14]!, 17, 0xa679438e);
b = ff(b, c, d, a, words[k + 15]!, 22, 0x49b40821);
a = gg(a, b, c, d, words[k + 1]!, 5, 0xf61e2562);
d = gg(d, a, b, c, words[k + 6]!, 9, 0xc040b340);
c = gg(c, d, a, b, words[k + 11]!, 14, 0x265e5a51);
b = gg(b, c, d, a, words[k]!, 20, 0xe9b6c7aa);
a = gg(a, b, c, d, words[k + 5]!, 5, 0xd62f105d);
d = gg(d, a, b, c, words[k + 10]!, 9, 0x02441453);
c = gg(c, d, a, b, words[k + 15]!, 14, 0xd8a1e681);
b = gg(b, c, d, a, words[k + 4]!, 20, 0xe7d3fbc8);
a = gg(a, b, c, d, words[k + 9]!, 5, 0x21e1cde6);
d = gg(d, a, b, c, words[k + 14]!, 9, 0xc33707d6);
c = gg(c, d, a, b, words[k + 3]!, 14, 0xf4d50d87);
b = gg(b, c, d, a, words[k + 8]!, 20, 0x455a14ed);
a = gg(a, b, c, d, words[k + 13]!, 5, 0xa9e3e905);
d = gg(d, a, b, c, words[k + 2]!, 9, 0xfcefa3f8);
c = gg(c, d, a, b, words[k + 7]!, 14, 0x676f02d9);
b = gg(b, c, d, a, words[k + 12]!, 20, 0x8d2a4c8a);
a = hh(a, b, c, d, words[k + 5]!, 4, 0xfffa3942);
d = hh(d, a, b, c, words[k + 8]!, 11, 0x8771f681);
c = hh(c, d, a, b, words[k + 11]!, 16, 0x6d9d6122);
b = hh(b, c, d, a, words[k + 14]!, 23, 0xfde5380c);
a = hh(a, b, c, d, words[k + 1]!, 4, 0xa4beea44);
d = hh(d, a, b, c, words[k + 4]!, 11, 0x4bdecfa9);
c = hh(c, d, a, b, words[k + 7]!, 16, 0xf6bb4b60);
b = hh(b, c, d, a, words[k + 10]!, 23, 0xbebfbc70);
a = hh(a, b, c, d, words[k + 13]!, 4, 0x289b7ec6);
d = hh(d, a, b, c, words[k]!, 11, 0xeaa127fa);
c = hh(c, d, a, b, words[k + 3]!, 16, 0xd4ef3085);
b = hh(b, c, d, a, words[k + 6]!, 23, 0x04881d05);
a = hh(a, b, c, d, words[k + 9]!, 4, 0xd9d4d039);
d = hh(d, a, b, c, words[k + 12]!, 11, 0xe6db99e5);
c = hh(c, d, a, b, words[k + 15]!, 16, 0x1fa27cf8);
b = hh(b, c, d, a, words[k + 2]!, 23, 0xc4ac5665);
a = ii(a, b, c, d, words[k]!, 6, 0xf4292244);
d = ii(d, a, b, c, words[k + 7]!, 10, 0x432aff97);
c = ii(c, d, a, b, words[k + 14]!, 15, 0xab9423a7);
b = ii(b, c, d, a, words[k + 5]!, 21, 0xfc93a039);
a = ii(a, b, c, d, words[k + 12]!, 6, 0x655b59c3);
d = ii(d, a, b, c, words[k + 3]!, 10, 0x8f0ccc92);
c = ii(c, d, a, b, words[k + 10]!, 15, 0xffeff47d);
b = ii(b, c, d, a, words[k + 1]!, 21, 0x85845dd1);
a = ii(a, b, c, d, words[k + 8]!, 6, 0x6fa87e4f);
d = ii(d, a, b, c, words[k + 15]!, 10, 0xfe2ce6e0);
c = ii(c, d, a, b, words[k + 6]!, 15, 0xa3014314);
b = ii(b, c, d, a, words[k + 13]!, 21, 0x4e0811a1);
a = ii(a, b, c, d, words[k + 4]!, 6, 0xf7537e82);
d = ii(d, a, b, c, words[k + 11]!, 10, 0xbd3af235);
c = ii(c, d, a, b, words[k + 2]!, 15, 0x2ad7d2bb);
b = ii(b, c, d, a, words[k + 9]!, 21, 0xeb86d391);
a = addUnsigned(a, aa);
b = addUnsigned(b, bb);
c = addUnsigned(c, cc);
d = addUnsigned(d, dd);
}
return (wordToHex(a) + wordToHex(b) + wordToHex(c) + wordToHex(d)).toLowerCase();
}
+54
View File
@@ -0,0 +1,54 @@
const WIRE_VARINT = 0;
const WIRE_BYTES = 2;
const WIRE_FIXED32 = 5;
export class ProtobufWriter {
private readonly buf: number[] = [];
private writeVarint(value: number): this {
let v = value >>> 0;
while (v >= 0x80) {
this.buf.push((v & 0x7f) | 0x80);
v >>>= 7;
}
this.buf.push(v & 0x7f);
return this;
}
private writeKey(fieldNumber: number, wireType: number): this {
return this.writeVarint((fieldNumber << 3) | wireType);
}
varint(fieldNumber: number, value: number): this {
return this.writeKey(fieldNumber, WIRE_VARINT).writeVarint(value);
}
fixed32(fieldNumber: number, value: number): this {
this.writeKey(fieldNumber, WIRE_FIXED32);
const v = value >>> 0;
this.buf.push(v & 0xff, (v >>> 8) & 0xff, (v >>> 16) & 0xff, (v >>> 24) & 0xff);
return this;
}
bytes(fieldNumber: number, data: ArrayLike<number>): this {
this.writeKey(fieldNumber, WIRE_BYTES).writeVarint(data.length);
for (let i = 0; i < data.length; i++) {
this.buf.push(data[i]! & 0xff);
}
return this;
}
string(fieldNumber: number, value: string): this {
return this.bytes(fieldNumber, new TextEncoder().encode(value));
}
message(fieldNumber: number, build: (writer: ProtobufWriter) => void): this {
const sub = new ProtobufWriter();
build(sub);
return this.bytes(fieldNumber, sub.toBytes());
}
toBytes(): Uint8Array<ArrayBuffer> {
return Uint8Array.from(this.buf);
}
}
+56
View File
@@ -0,0 +1,56 @@
import { hash } from "./index.ts";
import { getArgus } from "./argus.ts";
import { getLadon } from "./ladon.ts";
import { defaultUnidbgConfig, type UnidbgConfig } from "./unidbgconfig.ts";
export interface UnidbgHeaders extends Record<string, string> {
"x-argus": string;
"x-ladon": string;
"x-khronos": string;
}
export default async function generateHeaders(
rawQuery: string,
xssStub = "",
timestamp: number = Math.floor(Date.now() / 1000),
config: UnidbgConfig = defaultUnidbgConfig,
): Promise<UnidbgHeaders> {
const [argus, ladon] = await Promise.all([
getArgus(rawQuery, xssStub, timestamp, config),
getLadon(timestamp, config),
]);
return {
"x-argus": argus,
"x-ladon": ladon,
"x-khronos": String(timestamp),
};
}
export async function signRequest(
url: string,
body?: ArrayBuffer | Uint8Array<ArrayBuffer> | string,
config: UnidbgConfig = defaultUnidbgConfig,
): Promise<Record<string, string>> {
const rawQuery = new URL(url).search.replace(/^\?/, "");
const hasBody = typeof body === "string" ? body.length > 0 : (body?.byteLength ?? 0) > 0;
const xssStub = hasBody ? await hash.md5(body!) : "";
const now = Date.now();
const headers: Record<string, string> = await generateHeaders(
rawQuery,
xssStub,
Math.floor(now / 1000),
config,
);
headers["x-ss-req-ticket"] = String(now);
if (hasBody) {
headers["X-SS-STUB"] = xssStub;
}
return headers;
}
export { getArgus } from "./argus.ts";
export { getLadon } from "./ladon.ts";
export { defaultUnidbgConfig, type UnidbgConfig } from "./unidbgconfig.ts";
+47
View File
@@ -0,0 +1,47 @@
const ROUNDS = 72;
const MASK64 = 0xffffffffffffffffn;
const Z4 = 0x3dc94c3a046d678bn;
function getBit(value: bigint, position: number): bigint {
return (value >> BigInt(position)) & 1n;
}
function rotateLeft64(v: bigint, n: bigint): bigint {
return ((v << n) | (v >> (64n - n))) & MASK64;
}
function rotateRight64(v: bigint, n: bigint): bigint {
return ((v << (64n - n)) | (v >> n)) & MASK64;
}
function keyExpansion(key: readonly [bigint, bigint, bigint, bigint]): bigint[] {
const k: bigint[] = [key[0] & MASK64, key[1] & MASK64, key[2] & MASK64, key[3] & MASK64];
for (let i = 4; i < ROUNDS; i++) {
let tmp = rotateRight64(k[i - 1]!, 3n);
tmp ^= k[i - 3]!;
tmp ^= rotateRight64(tmp, 1n);
k.push((~k[i - 4]! ^ tmp ^ getBit(Z4, (i - 4) % 62) ^ 3n) & MASK64);
}
return k;
}
export function simonEncrypt(
plaintext: readonly [bigint, bigint],
key: readonly [bigint, bigint, bigint, bigint],
): [bigint, bigint] {
const k = keyExpansion(key);
let x = plaintext[0] & MASK64;
let y = plaintext[1] & MASK64;
for (let i = 0; i < ROUNDS; i++) {
const tmp = y;
const f = rotateLeft64(y, 1n) & rotateLeft64(y, 8n);
y = (x ^ f ^ rotateLeft64(y, 2n) ^ k[i]!) & MASK64;
x = tmp;
}
return [x, y];
}
+158
View File
@@ -0,0 +1,158 @@
// Thanks to https://github.com/JuneAndGreen/sm-crypto/blob/master/src/sm2/sm3.js
type Bytes = ArrayLike<number>;
/**
* 循环左移
*/
function rotl(value: number, shift: number): number {
const normalizedShift = shift & 31;
return (value << normalizedShift) | (value >>> (32 - normalizedShift));
}
/**
* 二进制异或运算
*/
function xor(left: Bytes, right: Bytes): Uint8Array {
if (left.length !== right.length) {
throw new Error("XOR inputs must have the same length");
}
const result = new Uint8Array(left.length);
for (let i = 0; i < left.length; i++) {
result[i] = (left[i]! ^ right[i]!) & 0xff;
}
return result;
}
/**
* 压缩函数中的置换函数 P0(X) = X xor (X <<< 9) xor (X <<< 17)
*/
function p0(value: number): number {
return (value ^ rotl(value, 9)) ^ rotl(value, 17);
}
/**
* 消息扩展中的置换函数 P1(X) = X xor (X <<< 15) xor (X <<< 23)
*/
function p1(value: number): number {
return (value ^ rotl(value, 15)) ^ rotl(value, 23);
}
/**
* 计算 SM3 摘要。
*/
export function sm3(input: Bytes): Uint8Array<ArrayBuffer> {
const inputLength = input.length;
const paddedLength = Math.ceil((inputLength + 9) / 64) * 64;
const message = new Uint8Array(paddedLength);
message.set(input);
message[inputLength] = 0x80;
const bitLength = BigInt(inputLength) * 8n;
for (let i = 0; i < 8; i++) {
message[paddedLength - 1 - i] = Number((bitLength >> BigInt(i * 8)) & 0xffn);
}
const dataView = new DataView(message.buffer);
const state = new Uint32Array([
0x7380166f,
0x4914b2b9,
0x172442d7,
0xda8a0600,
0xa96f30bc,
0x163138aa,
0xe38dee4d,
0xb0fb0e4e,
]);
const words = new Uint32Array(68);
const expandedWords = new Uint32Array(64);
for (let block = 0; block < message.length / 64; block++) {
const start = block * 64;
for (let i = 0; i < 16; i++) {
words[i] = dataView.getUint32(start + i * 4, false);
}
for (let i = 16; i < 68; i++) {
words[i] = (p1((words[i - 16]! ^ words[i - 9]!) ^ rotl(words[i - 3]!, 15))
^ rotl(words[i - 13]!, 7))
^ words[i - 6]!;
}
for (let i = 0; i < 64; i++) {
expandedWords[i] = words[i]! ^ words[i + 4]!;
}
let a = state[0]!;
let b = state[1]!;
let c = state[2]!;
let d = state[3]!;
let e = state[4]!;
let f = state[5]!;
let g = state[6]!;
let h = state[7]!;
for (let i = 0; i < 64; i++) {
const t = i <= 15 ? 0x79cc4519 : 0x7a879d8a;
const ss1 = rotl(rotl(a, 12) + e + rotl(t, i), 7);
const ss2 = ss1 ^ rotl(a, 12);
const tt1 = (i <= 15 ? (a ^ b) ^ c : ((a & b) | (a & c)) | (b & c))
+ d + ss2 + expandedWords[i]!;
const tt2 = (i <= 15 ? (e ^ f) ^ g : (e & f) | (~e & g))
+ h + ss1 + words[i]!;
d = c;
c = rotl(b, 9);
b = a;
a = tt1;
h = g;
g = rotl(f, 19);
f = e;
e = p0(tt2);
}
state[0] = state[0]! ^ a;
state[1] = state[1]! ^ b;
state[2] = state[2]! ^ c;
state[3] = state[3]! ^ d;
state[4] = state[4]! ^ e;
state[5] = state[5]! ^ f;
state[6] = state[6]! ^ g;
state[7] = state[7]! ^ h;
}
const result = new Uint8Array(32);
for (let i = 0; i < state.length; i++) {
const word = state[i]!;
result[i * 4] = word >>> 24;
result[i * 4 + 1] = word >>> 16;
result[i * 4 + 2] = word >>> 8;
result[i * 4 + 3] = word;
}
return result;
}
/**
* 计算 HMAC-SM3。
*/
export function hmac(input: Bytes, key: Bytes): Uint8Array<ArrayBuffer> {
const blockLength = 64;
const normalizedKey = key.length > blockLength ? sm3(key) : Uint8Array.from(key);
const paddedKey = new Uint8Array(blockLength);
paddedKey.set(normalizedKey);
const innerPad = new Uint8Array(blockLength).fill(0x36);
const outerPad = new Uint8Array(blockLength).fill(0x5c);
const innerKey = xor(paddedKey, innerPad);
const outerKey = xor(paddedKey, outerPad);
const innerMessage = new Uint8Array(blockLength + input.length);
innerMessage.set(innerKey);
innerMessage.set(input, blockLength);
const outerMessage = new Uint8Array(blockLength + 32);
outerMessage.set(outerKey);
outerMessage.set(sm3(innerMessage), blockLength);
return sm3(outerMessage);
}
+79
View File
@@ -0,0 +1,79 @@
import { hash, pkcs7Pad } from "./index.ts";
const ROUNDS = 34;
const MASK64 = 0xffffffffffffffffn;
const WORD_SIZE = 64n;
const ALPHA = 8n;
const BETA = 3n;
function readUint64LE(view: DataView, offset: number): bigint {
return view.getBigUint64(offset, true);
}
function keySchedule(key: Uint8Array): bigint[] {
const view = new DataView(key.buffer, key.byteOffset, key.byteLength);
const ks: bigint[] = [readUint64LE(view, 0) & MASK64];
const numWords = (key.length * 8) / Number(WORD_SIZE);
const ls: bigint[] = [];
for (let i = 1; i < numWords; i++) {
ls.push(readUint64LE(view, i * 8) & MASK64);
}
for (let x = 0; x < ROUNDS - 1; x++) {
const rsX = (((ls[x]! << (WORD_SIZE - ALPHA)) + (ls[x]! >> ALPHA)) & MASK64);
const addSxy = (rsX + ks[x]!) & MASK64;
const newX = BigInt(x) ^ addSxy;
const lsY = (((ks[x]! >> (WORD_SIZE - BETA)) + (ks[x]! << BETA)) & MASK64);
ls.push(newX);
ks.push(newX ^ lsY);
}
return ks;
}
function encryptBlock(ks: readonly bigint[], block: Uint8Array, out: Uint8Array, outOffset: number): void {
const view = new DataView(block.buffer, block.byteOffset, block.byteLength);
let y = readUint64LE(view, 0);
let x = readUint64LE(view, 8);
for (const k of ks) {
const rsX = (((x << (WORD_SIZE - ALPHA)) + (x >> ALPHA)) & MASK64);
const addSxy = (rsX + y) & MASK64;
x = k ^ addSxy;
const lsY = (((y >> (WORD_SIZE - BETA)) + (y << BETA)) & MASK64);
y = x ^ lsY;
}
const outView = new DataView(out.buffer, out.byteOffset, out.byteLength);
outView.setBigUint64(outOffset, y & MASK64, true);
outView.setBigUint64(outOffset + 8, x & MASK64, true);
}
export function speckEncrypt(key: Uint8Array, plaintext: Uint8Array): Uint8Array<ArrayBuffer> {
if (key.length !== 32) {
throw new Error(`Speck key must be 32 bytes, got ${key.length}`);
}
const padded = pkcs7Pad(plaintext, 16);
const ks = keySchedule(key);
const out = new Uint8Array(padded.length);
for (let i = 0; i < padded.length; i += 16) {
encryptBlock(ks, padded.subarray(i, i + 16), out, i);
}
return out;
}
export async function generateLadonKey(randomBytes: Uint8Array, aid: string): Promise<Uint8Array<ArrayBuffer>> {
const aidBytes = new TextEncoder().encode(aid);
const input = new Uint8Array(randomBytes.length + aidBytes.length);
input.set(randomBytes);
input.set(aidBytes, randomBytes.length);
const hex = await hash.md5(input);
return new TextEncoder().encode(hex);
}
+26
View File
@@ -0,0 +1,26 @@
// TT 加密(设备注册请求体):gzip + SHA512 校验 + AES-128-CBC,magic 头 "tc"
// 移植自 fanqie-assistant src/crypto/ttencrypt.ts(Node crypto 实现)
import { createCipheriv, createHash, randomBytes } from 'node:crypto'
import { gzipSync } from 'node:zlib'
// 固定盐(base64 常量,社区共知)
const FIXED_STRING = Buffer.from(
'TdTC5rgxYgkOUrPHpnM7pByyRiuCmrWKGWs521cXdST0m69/COjWjSanLjfBqVovHwWlGJKu8pSXMrYqOKrdWA==',
'base64',
)
const sha512 = (data: Buffer): Buffer => createHash('sha512').update(data).digest()
export const encryptTT = (data: Buffer): Buffer => {
const rand = randomBytes(32)
const hashValue = sha512(Buffer.concat([sha512(rand), FIXED_STRING]))
const key = hashValue.subarray(0, 16)
const iv = hashValue.subarray(16, 32)
const compressed = gzipSync(data)
const payload = Buffer.concat([sha512(compressed), compressed])
const cipher = createCipheriv('aes-128-cbc', key, iv)
const encrypted = Buffer.concat([cipher.update(payload), cipher.final()])
return Buffer.concat([Buffer.from([116, 99, 5, 16, 0, 0]), rand, encrypted])
}
export default { encryptTT }
+27
View File
@@ -0,0 +1,27 @@
// 汽水音乐(luna, aid=386088)签名配置
// signKey/licenseId 是字节 metasec SDK 家族(v04.04.05-ov-android)共用密钥——经 ssovit(TikTok
// aid=1233)/fanqie-assistant(番茄 aid=1967)/本项目(汽水 aid=386088) 三方同源实证;per-app 的只有
// 载荷里的 msAppID(argus protobuf field 4,由 config.aid 提供)。metasec license blob(native
// SDK 运行时 init 用)不参与离线签名。
import { unhex } from "./index.ts";
export interface UnidbgConfig {
signKey: Uint8Array;
aid: string;
licenseId: string;
sdkVersion: string;
sdkVersionInt: number;
callType: number;
secDeviceId?: string;
}
export const defaultUnidbgConfig: UnidbgConfig = {
signKey: new Uint8Array(
unhex("ac1adaae95a7af94a5114ab3b3a97dd80050aa0a39314c40528caec95256c28c"),
),
aid: "386088",
licenseId: "1611921764",
sdkVersion: "v04.04.05-ov-android",
sdkVersionInt: 134744640,
callType: 738,
};