Reversing Youdao Translate Sign Generation and Response Decryption in Python

Generating the sign Parameter

The sign parameter obsevred in Youdao's translate API appears as a 32-character hexadecimal string, indicative of MD5 hashing.

Trace the JavaScript logic to locate where sign is produced:

const clientId = "fanyideskweb";
const productTag = "webfanyi";

function md5Hex(input) {
  return require('crypto').createHash('md5').update(String(input)).digest('hex');
}

function buildSign(ts, secret) {
  const raw = `client=${clientId}&mysticTime=${ts}&product=${productTag}&key=${secret}`;
  return md5Hex(raw);
}

function assembleParams(secret) {
  const now = Date.now();
  return { sign: buildSign(now, secret) };
}

During dynamic aanlysis, two distinct calls to buildSign occur. The second call uses a dynamic secret value (Vy4EQ1uwPkUoqvcP1nIu6WiAjxFeA3Y3) tied to the actual translation request. This must be used for reproducing the signature.

Node.js version:

const crypto = require('crypto');

const clientId = "fanyideskweb";
const productTag = "webfanyi";
const fixedSecret = "Vy4EQ1uwPkUoqvcP1nIu6WiAjxFeA3Y3";

function md5Hex(val) {
  return crypto.createHash('md5').update(val).digest('hex');
}

function makeSignature(ts, sec) {
  const payload = `client=${clientId}&mysticTime=${ts}&product=${productTag}&key=${sec}`;
  return md5Hex(payload);
}

function produceSign() {
  const moment = Date.now();
  return { sign: makeSignature(moment, fixedSecret) };
}

console.log(produceSign());

Python equivalent:

import time
import hashlib

ts = str(int(time.time() * 1000))

def compute_sign():
    raw = f"client=fanyideskweb&mysticTime={ts}&product=webfanyi&key=Vy4EQ1uwPkUoqvcP1nIu6WiAjxFeA3Y3"
    return hashlib.md5(raw.encode()).hexdigest()

print(compute_sign())

Decrypting the Encrypted Response

The API returns encrypted text that must be decoded via AES-128-CBC using static key material.

Locate the decryption routine in the JS source:

const keyMaterial = 'ydsecret://query/key/B*RGygVywfNBwpmBaZg*WT7SIOUP2T0C9WHMZN39j^DAdaZhAnxvGcCY6VYFwnHl';
const ivMaterial  = 'ydsecret://query/iv/C@lZe2YzHtZ2CYgaXKSVfsb7Y4QWHjITPPZ0nQp87fBeJ!Iv6v^6fvi2WN@bYpJ4';

function md5Bytes(str) {
  return crypto.createHash('md5').update(str).digest();
}

function decipherPayload(b64Text) {
  const keyBuf = Buffer.alloc(16, md5Bytes(keyMaterial));
  const ivBuf  = Buffer.alloc(16, md5Bytes(ivMaterial));
  const decipher = crypto.createDecipheriv('aes-128-cbc', keyBuf, ivBuf);
  let plain = decipher.update(b64Text, 'base64', 'utf-8');
  plain += decipher.final('utf-8');
  return plain;
}

// b64Text is the encrypted response
console.log(decipherPayload(b64Text));

Python implemantation:

import base64
import hashlib
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad

def decode_response(b64_input):
    key_raw = "ydsecret://query/key/B*RGygVywfNBwpmBaZg*WT7SIOUP2T0C9WHMZN39j^DAdaZhAnxvGcCY6VYFwnHl"
    iv_raw  = "ydsecret://query/iv/C@lZe2YzHtZ2CYgaXKSVfsb7Y4QWHjITPPZ0nQp87fBeJ!Iv6v^6fvi2WN@bYpJ4"
    key = hashlib.md5(key_raw.encode()).digest()
    iv  = hashlib.md5(iv_raw.encode()).digest()
    cipher = AES.new(key, AES.MODE_CBC, iv)
    raw = base64.urlsafe_b64decode(b64_input)
    clear = unpad(cipher.decrypt(raw), AES.block_size)
    return clear.decode()

Complete Python Workflow

import time
import requests
import base64
import hashlib
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad

ts = str(int(time.time() * 1000))

def generate_signature():
    src = f"client=fanyideskweb&mysticTime={ts}&product=webfanyi&key=Vy4EQ1uwPkUoqvcP1nIu6WiAjxFeA3Y3"
    return hashlib.md5(src.encode()).hexdigest()

def fetch_encrypted(text):
    hdr = {
        'Referer': 'https://fanyi.youdao.com/',
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 '
                      '(KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36',
    }
    payload = {
        'i': text,
        'from': 'en',
        'to': 'zh-CHS',
        'useTerm': 'false',
        'domain': '0',
        'dictResult': 'true',
        'keyid': 'webfanyi',
        'sign': generate_signature(),
        'client': 'fanyideskweb',
        'product': 'webfanyi',
        'appVersion': '1.0.0',
        'vendor': 'web',
        'pointParam': 'client,mysticTime,product',
        'mysticTime': ts,
        'keyfrom': 'fanyi.web',
        'mid': '1',
        'screen': '1',
        'model': '1',
        'network': 'wifi',
        'abtest': '0',
        'yduuid': 'abcdefg',
    }
    resp = requests.post('https://dict.youdao.com/webtranslate', headers=hdr, data=payload).text
    return resp

def decrypt_response(b64_text):
    km = "ydsecret://query/key/B*RGygVywfNBwpmBaZg*WT7SIOUP2T0C9WHMZN39j^DAdaZhAnxvGcCY6VYFwnHl"
    iv = "ydsecret://query/iv/C@lZe2YzHtZ2CYgaXKSVfsb7Y4QWHjITPPZ0nQp87fBeJ!Iv6v^6fvi2WN@bYpJ4"
    key = hashlib.md5(km.encode()).digest()
    iv_bytes = hashlib.md5(iv.encode()).digest()
    cipher = AES.new(key, AES.MODE_CBC, iv_bytes)
    data = base64.urlsafe_b64decode(b64_text)
    plain = unpad(cipher.decrypt(data), AES.block_size)
    return plain.decode()

if __name__ == '__main__':
    word = 'apple'
    enc = fetch_encrypted(word)
    print(decrypt_response(enc))

Tags: python web scraping youdao translate js reverse engineering aes decryption

Posted on Sat, 09 May 2026 03:23:40 +0000 by explorer