吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 6272|回复: 52
上一主题 下一主题
收起左侧

[Web逆向] 某道翻译请求关键参数和返回数据解密过程分析-20230405

  [复制链接]
跳转到指定楼层
楼主
hans7 发表于 2023-4-5 19:38 回帖奖励
本帖最后由 hans7 于 2023-4-7 01:27 编辑

引言

今天本英语渣用了下谋道翻译,惊讶地发现谋道返回的接口数据是加密的。想着我已经很久没碰逆向了,那就来研究一下吧,顺便水篇入门文。PS:整个过程没有用到动态调试。

作者:hans774882968以及hans774882968以及hans774882968

本文52pojie:https://www.52pojie.cn/thread-1769988-1-1.html

本文juejin:https://juejin.cn/post/7218487123212664890/

本文CSDN:https://blog.csdn.net/hans774882968/article/details/129976697

webtranslate接口返回加密数据的解密过程

  1. 谋道翻译webtranslate接口:https://dict.moudao.com/webtranslate
  2. 谋道翻译webtranslate获取secretKey接口:https://dict.moudao.com/webtranslate/key

抓包,找到附近代码:

nn["a"].getTextTranslateResult({
    i: e.data.keyword,
    from: e.data.from,
    to: e.data.to,
    ...n,
    dictResult: !0,
    keyid: "webfanyi"
}, o).then(o=>{
    nn["a"].cancelLastGpt();
    const n = nn["a"].decodeData(o, an["a"].state.text.decodeKey, an["a"].state.text.decodeIv)
      , a = n ? JSON.parse(n) : {};
    console.log("解密后的接口数据:", a), // 谋道故意放水,直接给答案?
    0 === a.code ? e.success && t(e.success)(a) : e.fail && t(e.fail)(a)
}

o就是接口加密数据,主要需要确定decodeKey, decodeIv。因为某道翻译前端是webpack打包的应用,所以可以这么找nn, an的位置:

首先看到

var nn = o("8139")
  , an = o("4360")
  , sn = o("bc3a");

打开Chrome Devtools的Search Tab,搜索8139,很快找到(https://fanyi.moudao.com/js/app.e4e9fbd0.js

8139: function(e, t, o) {
  "use strict";
  // ...
},
8393: //...

于是可知decodeData内容:

T = (t,o,n)=>{
    if (!t)
        return null;
    const a = e.alloc(16, f(o))
      , i = e.alloc(16, f(n))
      , r = c.a.createDecipheriv("aes-128-cbc", a, i);
    let s = r.update(t, "base64", "utf-8");
    return s += r.final("utf-8"),
    s
}

不过decodeKeydecodeIv并不是很好定位……还是通过Chrome Devtools的Search Tab直接搜到的。

const i = {
    secretKey: "",
    dictResult: {},
    decodeKey: "ydsecret://query/key/B*RGygVywfNBwpmBaZg*WT7SIOUP2T0C9WHMZN39j^DAdaZhAnxvGcCY6VYFwnHl",
    decodeIv: "ydsecret://query/iv/C@lZe2YzHtZ2CYgaXKSVfsb7Y4QWHjITPPZ0nQp87fBeJ!Iv6v^6fvi2WN@bYpJ4",
    allowStroke: !1,
    showPjm: !1,
    showRomanPronunciation: !1,
    showWordsNumber: !0
}

知道这两个变量的值以后,也可以马后炮地倒推一下4360这个模块做的事:

4360: function(e, t, o) {
    "use strict";
    o("13d5");
    var n = o("5502");
    const a = []
      , c = o("c653")
      , i = c.keys().reduce((e,t)=>{ // c.keys()取出的是["./domain.js"]数组
        const o = t.replace(/^\.\/(.*)\.\w+$/, "$1");
        a.push(o);
        const n = c(t);
        return e[o] = n.default,
        e
    }
    , {});
    t["a"] = Object(n["a"])({
        modules: i
    })
},

i变量可以猜测是导出的模块,c = o("c653")似乎在做模块整合。

    c653: function(e, t, o) {
        var n = {
            "./domain.js": "d2a7",
            "./language.js": "c083",
            "./login.js": "b5ce",
            "./text.js": "1a68"
        };
        function a(e) {
            var t = c(e);
            return o(t)
        }
        function c(e) {
            if (!o.o(n, e)) {
                var t = new Error("Cannot find module '" + e + "'");
                throw t.code = "MODULE_NOT_FOUND",
                t
            }
            return n[e]
        }
        a.keys = function() {
            return Object.keys(n)
        }
        ,
        a.resolve = c,
        e.exports = a,
        a.id = "c653"
    },

看到./text.js,结合an["a"].state.text.decodeKey可以猜测1a68就是关键模块。

    "1a68": function(e, t, o) {
        "use strict";
        o.r(t);
        var n = o("8139")
          , a = o("8544")
          , c = o("c34f");
        const i = {
            secretKey: "",
            dictResult: {},
            decodeKey: "ydsecret://query/key/B*RGygVywfNBwpmBaZg*WT7SIOUP2T0C9WHMZN39j^DAdaZhAnxvGcCY6VYFwnHl",
            decodeIv: "ydsecret://query/iv/C@lZe2YzHtZ2CYgaXKSVfsb7Y4QWHjITPPZ0nQp87fBeJ!Iv6v^6fvi2WN@bYpJ4",
            allowStroke: !1,
            showPjm: !1,
            showRomanPronunciation: !1,
            showWordsNumber: !0
        }
          , r = {
            secretKey: e=>e.secretKey,
            dictResult: e=>e.dictResult
        }
          , s = {
            fetchTextTranslateSecretKey: ({commit: e},t)=>{
                const o = "webfanyi-key-getter"
                  , a = "asdjnjfenknafdfsdfsd";
                n["a"].getTextTranslateSecretKey({
                    keyid: o
                }, a).then(t=>{
                    0 === t.code && t.data.secretKey && e("UPDATE_SECRET_KEY", t.data.secretKey)
                }
                ).catch(e=>{}
                )
            }
            ,
            setDictResult: ({commit: e},t)=>{
                e("SET_DICTRESULT", t)
            }
            ,
            initTextTranslateSettingStore: ({commit: e},t)=>{
                const o = a["a"].get("allowStroke")
                  , n = a["a"].get("showPjm")
                  , c = a["a"].get("showRomanPronunciation")
                  , i = a["a"].get("showWordsNumber");
                e("SET_ALLOW_STROKE", null !== o && o),
                e("SET_SHOW_PJM", null !== n && n),
                e("SET_SHOW_ROMAN_PRONUNCICATION", null !== c && c),
                e("SET_SHOW_WORDS_NUMBER", null === i || i)
            }
        }
          , l = {
            UPDATE_SECRET_KEY(e, t) {
                e.secretKey = t
            },
            SET_DICTRESULT(e, t) {
                e.dictResult = t
            },
            SET_ALLOW_STROKE(e, t) {
                e.allowStroke = t,
                a["a"].set("allowStroke", t),
                Object(c["b"])(t)
            },
            SET_SHOW_PJM(e, t) {
                e.showPjm = t,
                a["a"].set("showPjm", t)
            },
            SET_SHOW_ROMAN_PRONUNCICATION(e, t) {
                e.showRomanPronunciation = t,
                a["a"].set("showRomanPronunciation", t)
            },
            SET_SHOW_WORDS_NUMBER(e, t) {
                e.showWordsNumber = t,
                a["a"].set("showWordsNumber", t)
            }
        };
        t["default"] = {
            state: i,
            getters: r,
            mutations: l,
            actions: s
        }
    },

显然1a68是一个vuex模块🐔⌨️🍚,这个模块在后文《webtranslate接口的sign参数生成过程分析》还会用到。

至此,decodeData3个参数都知道了,分析下其内容:

T = (t,o,n)=>{
    if (!t)
        return null;
    const a = e.alloc(16, f(o))
      , i = e.alloc(16, f(n))
      , r = c.a.createDecipheriv("aes-128-cbc", a, i);
    let s = r.update(t, "base64", "utf-8");
    return s += r.final("utf-8"),
    s
}
  1. e是什么?它出现在chunk-vendors里,根据webpack常识,chunk-vendors一般就是标准库。注释里有一句The buffer module from node.js, for the browser.,所以e就是node.js Buffer的polyfill。
  2. c.a是什么?createDecipheriv也出现在chunk-vendors,所以也属于标准库,网上搜一下createDecipheriv可知c.anode.js自带的crypto模块。
  3. f就是同一个模块定义的函数:
function f(e) {
    return c.a.createHash("md5").update(e).digest()
}

至此,我们已经可以写出解密代码:

const crypto = require('crypto');

function getMd5(e) {
  return crypto.createHash('md5').update(e).digest();
}

function decryptData(encryptedText) {
  const algo = 'aes-128-cbc';
  const decodeKey = 'ydsecret://query/key/B*RGygVywfNBwpmBaZg*WT7SIOUP2T0C9WHMZN39j^DAdaZhAnxvGcCY6VYFwnHl';
  const decodeIv = 'ydsecret://query/iv/C@lZe2YzHtZ2CYgaXKSVfsb7Y4QWHjITPPZ0nQp87fBeJ!Iv6v^6fvi2WN@bYpJ4';
  const md5Key = Buffer.alloc(16, getMd5(decodeKey));
  const md5Iv = Buffer.alloc(16, getMd5(decodeIv));
  const decipher = crypto.createDecipheriv(algo, md5Key, md5Iv);
  let res = decipher.update(encryptedText, 'base64', 'utf-8');
  res += decipher.final('utf-8');
  return res;
}

function getTranslateResult(text) {
  const o = JSON.parse(text);
  return o.translateResult[0].reduce((res, item) => {
    return res + item.tgt;
  }, '');
}

const encryptedText = '<接口的返回值>';
const text = decryptData(encryptedText);
const translateResult = getTranslateResult(text);
console.log(translateResult);

题外话:为什么某道翻译会有一句console.log("解密后的接口数据", a),并且a比我们得到的解密结果多了几个参数?因为这个对象在接口数据解密后被追加了一些属性。

webtranslate接口的sign参数生成过程分析

首先还是考虑搜关键词:sign不仅要在前端生成,还要在后端校验,所以一般来说,sign要求能通过这个接口的其他参数生成,因此我们挑选了mysticTime。我们搜到一个名为8139的模块:

const l = "fanyideskweb"
  , d = "webfanyi"
  , u = "client,mysticTime,product"
  , m = "1.0.0"
  , p = "web"
  , b = "fanyi.web";
function f(e) {
    return c.a.createHash("md5").update(e).digest()
}
function g(e) {
    return c.a.createHash("md5").update(e.toString()).digest("hex")
}
function v(e, t) {
    return g(`client=${l}&mysticTime=${e}&product=${d}&key=${t}`)
}
function h(e) {
    const t = (new Date).getTime();
    return {
        sign: v(t, e),
        client: l,
        product: d,
        appVersion: m,
        vendor: p,
        pointParam: u,
        mysticTime: t,
        keyfrom: b
    }
}

根据上文分析结果,c.a就是node.js自带的crypto模块。于是只剩一个疑点了:h函数的e参数。往下可以翻到h的调用方式:

const A = (e,t)=>Object(n["a"])("https://dict.moudao.com/webtranslate/key", {
    ...e,
    ...h(t)
})
  , O = (e,t)=>Object(n["d"])("https://dict.moudao.com/webtranslate", {
    ...e,
    ...h(t)
}, {
    headers: {
        "Content-Type": "application/x-www-form-urlencoded"
    }
})

我们只需要确定箭头函数第二个参数t。对应的模块导出代码:

t["a"] = {
    getTextTranslateSecretKey: A,
    getTextTranslateResult: O,
    getTextTranslateKeyword: y,
    decodeData: T,
    feedback: x,
    getAigcEntrance: w,
    getAigcStyle: k,
    getAigcTran: C,
    fanyiFeedback: E,
    cancelLastGpt: j
}

O为例,我们搜索getTextTranslateResult,找到:

const o = an["a"].state.text.secretKey;
// ...
nn["a"].getTextTranslateResult({
  i: e.data.keyword,
  from: e.data.from,
  to: e.data.to,
  ...n,
  dictResult: !0,
  keyid: "webfanyi"
}, o)

看到熟悉的an["a"].state.text,所以答案就在上文分析提到的vuex模块。

const i = {
    secretKey: ""
}

难道secretKey就是空串?我们写代码,在node中运行,发现是错的。随后我在上述vuex模块中发现了修改secretKey的函数UPDATE_SECRET_KEY。我们搜一下:

fetchTextTranslateSecretKey: ({commit: e},t)=>{
    const o = "webfanyi-key-getter"
      , a = "asdjnjfenknafdfsdfsd";
    n["a"].getTextTranslateSecretKey({
        keyid: o
    }, a).then(t=>{
        0 === t.code && t.data.secretKey && e("UPDATE_SECRET_KEY", t.data.secretKey)
    }
    ).catch(e=>{}
    )
}

发现8139模块出现过getTextTranslateSecretKey这个关键词,所以我们需要从https://dict.moudao.com/webtranslate/key这个接口中拿到secretKey

综上,我们可以写出代码:

const crypto = require('crypto');

// mysticTime = (new Date).getTime();
function getSign(mysticTime, secretKey) {
  const l = "fanyideskweb"
    , d = "webfanyi"
    , u = "client,mysticTime,product"
    , m = "1.0.0"
    , p = "web"
    , b = "fanyi.web";

  function g(e) {
    return crypto.createHash("md5").update(e.toString()).digest("hex")
  }

  return g(`client=${l}&mysticTime=${mysticTime}&product=${d}&key=${secretKey}`);
}

console.log(getSign(1680688241299, 'fsdsogkndfokasodnaso') === '7c5dbf08b8e0ecdf6895f623f335a320');

结束了吗?还没!接下来看getTextTranslateSecretKey接口的请求参数,发现也有一个sign参数,我们还需要继续分析。我们很容易搜到以下代码:

fetchTextTranslateSecretKey: ({commit: e},t)=>{
    const o = "webfanyi-key-getter"
      , a = "asdjnjfenknafdfsdfsd";
    n["a"].getTextTranslateSecretKey({
        keyid: o
    }, a).then(t=>{
        0 === t.code && t.data.secretKey && e("UPDATE_SECRET_KEY", t.data.secretKey)
    }
    ).catch(e=>{}
    )
}

其密钥就是'asdjnjfenknafdfsdfsd'。至此,分析也就结束了。

const crypto = require('crypto');

// mysticTime = (new Date).getTime();
function getSign(mysticTime, secretKey) {
  const l = "fanyideskweb"
    , d = "webfanyi"
    , u = "client,mysticTime,product"
    , m = "1.0.0"
    , p = "web"
    , b = "fanyi.web";

  function g(e) {
    return crypto.createHash("md5").update(e.toString()).digest("hex")
  }

  return g(`client=${l}&mysticTime=${mysticTime}&product=${d}&key=${secretKey}`);
}

// getTextTranslateResult
console.log(getSign(1680688241299, 'fsdsogkndfokasodnaso') === '7c5dbf08b8e0ecdf6895f623f335a320');
// getTextTranslateSecretKey
console.log(getSign(1680688236068, 'asdjnjfenknafdfsdfsd') === 'f01914c8a1e374094258ed80b94d9abb')

梳理一下+cookie反爬补充+python代码~

相比于去年,难度提升太多了!

'asdjnjfenknafdfsdfsd'获取getTextTranslateSecretKey所需的sign参数,请求获取secretKey→由secretKey获取getTextTranslateResult所需的sign参数,请求获取翻译结果→aes-128-cbc获取解密后的翻译结果数据。

下面根据评论区 Sommuni 佬的代码补充python代码部分。因为我没有跟完全流程,所以还是错过了一些风景。某道有做cookie反爬,url:https://rlogs.youdao.com/rlog.php ,可以看到响应头有set-cookie:

Set-Cookie: OUTFOX_SEARCH_USER_ID=-1072836051@183.240.8.75; Domain=youdao.com; Expires=Sat, 29-Mar-2053 14:05:37 GMT; Path=/

不过,他的代码是上个月的,请求上述url没有带get参数也能过。现在请求上述url必须要带时间_ntms参数,才能得到set-cookie响应头。具体可参考我提供的python代码。

我的python代码是从他的代码修改得来:

  1. 增强可测试性。
  2. 没有写死cookie。
import requests
import hashlib
import base64
import time
import json
from Crypto.Cipher import AES
from Crypto.Hash import MD5

class YouDao:
    def __init__(self):
        self.l = "fanyideskweb"
        self.d = "webfanyi"
        self.AES_KEY = b"ydsecret://query/key/B*RGygVywfNBwpmBaZg*WT7SIOUP2T0C9WHMZN39j^DAdaZhAnxvGcCY6VYFwnHl"
        self.AES_IV = b"ydsecret://query/iv/C@lZe2YzHtZ2CYgaXKSVfsb7Y4QWHjITPPZ0nQp87fBeJ!Iv6v^6fvi2WN@bYpJ4"
        self.const_secret_key = "asdjnjfenknafdfsdfsd"
        self.sessionObj = requests.session()

    def encrypt_md5(self, str):
        md = hashlib.md5(str.encode('utf-8')).hexdigest()
        return md

    def set_cookie_request(self):
        t = int(time.time() * 1000)
        # var g = new Date(b.lastModified); E = g.getTime() / 1e3; // b =
        # document
        last_modified = (t - 14 * 86400000) // 1000
        url = f"https://rlogs.youdao.com/rlog.php?_npid=fanyiweb&_ncat=pageview&_ncoo=775934043.5983925&_nssn=NULL&_nver=1.2.0&_ntms={t}&_nref=http%3A%2F%2Ffanyi.youdao.com%2F&_nurl=https%3A%2F%2Ffanyi.youdao.com%2Findex.html%23%2F&_nres=1440x900&_nlmf={last_modified}&_njve=0&_nchr=utf-8&_nfrg=%2F&/=NULL&screen=1440*900"
        resp = self.sessionObj.get(url)
        print('https://rlogs.youdao.com/rlog.php headers', resp.headers)  # dbg
        print('self.sessionObj.cookies',
              self.sessionObj.cookies.items())  # dbg

    def prepare_secret_key_params(self):
        t = str(int(time.time() * 1000))
        sign = self.get_sign(t, self.const_secret_key)
        params = {
            "sign": sign,
            "client": "fanyideskweb",
            "product": "webfanyi",
            "appVersion": "1.0.0",
            "vendor": "web",
            "pointParam": "client,mysticTime,product",
            "mysticTime": t,
            "keyfrom": "fanyi.web",
            "keyid": "webfanyi-key-getter",
        }
        return params

    def get_secret_key(self):
        params = self.prepare_secret_key_params()
        secret_key_url = "https://dict.youdao.com/webtranslate/key"
        res = self.sessionObj.get(secret_key_url, params=params).json()
        secret_key = res["data"]["secretKey"]
        print(secret_key)  # dbg
        return secret_key

    def get_sign(self, t, key):
        sign = self.encrypt_md5(
            f"client={self.l}&mysticTime={t}&product={self.d}&key={key}")
        return sign

    def youdao_decrypt(self, src: str) -> dict:
        key = self.AES_KEY
        iv = self.AES_IV
        cryptor = AES.new(
            MD5.new(key).digest()[:16],
            AES.MODE_CBC,
            MD5.new(iv).digest()[:16]
        )
        res = cryptor.decrypt(base64.urlsafe_b64decode(src))
        txt = res.decode("utf-8")
        return json.loads(txt[:txt.rindex("}") + 1])

    def get_actual_translate_result(self, resultJSON):
        result = ''
        for arr in resultJSON["translateResult"]:
            for item in arr:
                result += item["tgt"]
        return result

    def prepare_translate_data(self, msg, secret_key):
        headers = {
            "Accept": "application/json, text/plain, */*",
            "Accept-Language": "zh-CN,zh;q=0.9",
            "Connection": "keep-alive",
            "Content-Type": "application/x-www-form-urlencoded",
            "Origin": "https://fanyi.youdao.com",
            "Referer": "https://fanyi.youdao.com/",
            "Sec-Fetch-Dest": "empty",
            "Sec-Fetch-Mode": "cors",
            "Sec-Fetch-Site": "same-site",
            "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36",
            "sec-ch-ua": "\"Chromium\";v=\"110\", \"Not A(Brand\";v=\"24\", \"Google Chrome\";v=\"110\"",
            "sec-ch-ua-mobile": "?0",
            "sec-ch-ua-platform": "\"macOS\""}
        t = str(int(time.time() * 1000))
        sign = self.get_sign(t, secret_key)
        data = {
            "i": f"{msg}",
            "from": "auto",
            "to": "",
            "dictResult": "true",
            "keyid": "webfanyi",
            "sign": f"{sign}",
            "client": "fanyideskweb",
            "product": "webfanyi",
            "appVersion": "1.0.0",
            "vendor": "web",
            "pointParam": "client,mysticTime,product",
            "mysticTime": f"{t}",
            "keyfrom": "fanyi.web"
        }
        return headers, data

    def translate(self, msg):
        self.set_cookie_request()
        secret_key = self.get_secret_key()
        headers, data = self.prepare_translate_data(msg, secret_key)
        translate_url = "https://dict.youdao.com/webtranslate"
        response = self.sessionObj.post(
            translate_url, headers=headers, data=data).text
        print(response)  # dbg
        resultJSON = self.youdao_decrypt(response)
        print(resultJSON)  # dbg
        result = self.get_actual_translate_result(resultJSON)
        return resultJSON, result

def get_input_text():
    fname = 'youdao_in.txt'
    with open(fname, 'r', encoding='utf-8') as f:
        return f.read()

if __name__ == '__main__':
    youDao = YouDao()
    msg = get_input_text()
    resultJSON, result = youDao.translate(msg)
    print(result)

谋道翻译用到的vuex

我们回过头来看4360模块:

4360: function(e, t, o) {
    "use strict";
    o("13d5");
    var n = o("5502");
    const a = []
      , c = o("c653")
      , i = c.keys().reduce((e,t)=>{ // c.keys()取出的是["./domain.js"]数组
        const o = t.replace(/^\.\/(.*)\.\w+$/, "$1");
        a.push(o);
        const n = c(t);
        return e[o] = n.default,
        e
    }
    , {});
    t["a"] = Object(n["a"])({
        modules: i
    })
},

这相当于

export default new Vuex.Store({
    modules: {
        domain: {},
        language: {},
        login: {},
        text: {},
    }
})

而每个模块都是{ actions, getters, mutations, state }的结构,以./text.js为例:

{
    actions: ...,
    getters: ...,
    mutations: ...,
    state: {
        "secretKey": "",
        "dictResult": {},
        "decodeKey": "ydsecret://query/key/B*RGygVywfNBwpmBaZg*WT7SIOUP2T0C9WHMZN39j^DAdaZhAnxvGcCY6VYFwnHl",
        "decodeIv": "ydsecret://query/iv/C@lZe2YzHtZ2CYgaXKSVfsb7Y4QWHjITPPZ0nQp87fBeJ!Iv6v^6fvi2WN@bYpJ4",
        "allowStroke": false,
        "showPjm": false,
        "showRomanPronunciation": false,
        "showWordsNumber": true
    }
}

使用时,an["a"].state.text.decodeKey相当于

import store from '@/store.js';

store.state.text.decodeKey;

免费评分

参与人数 11威望 +2 吾爱币 +111 热心值 +8 收起 理由
xmqxmq110 + 1 + 1 我很赞同!
笙若 + 1 + 1 谢谢@Thanks!
yxpp + 1 用心讨论,共获提升!
涛之雨 + 2 + 100 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
Courser + 1 + 1 谢谢@Thanks!
三滑稽甲苯 + 2 + 1 用心讨论,共获提升!
JIAN_ + 1 + 1 大为震撼!
random1 + 1 + 1 谢谢@Thanks!
Yangzaipython + 1 谢谢@Thanks!
萌新与小白 + 1 + 1 热心回复!
RikimaruMarlon + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

推荐
Sommuni 发表于 2023-4-6 13:46
可以提供一个python版本的供你们学习,这里额外提一句,调用接口翻译的时候需要加入cookie,有道有做cookie反爬,但实际不校验cookie有效,所以可以写死

[Python] 纯文本查看 复制代码
# -*- coding:UTF-8 -*-

# author:sommuni
# contact: [email]test@test.com[/email]
# datetime:2023/3/9 20:01
# software: PyCharm

"""
文件说明:
    
"""

import requests,hashlib,base64,time,json
from Crypto.Cipher import AES
from Crypto.Hash import MD5


class YouDao:

    def __init__(self):
        self.l = "fanyideskweb"
        self.d = "webfanyi"
        self.AES_KEY = b"ydsecret://query/key/B*RGygVywfNBwpmBaZg*WT7SIOUP2T0C9WHMZN39j^DAdaZhAnxvGcCY6VYFwnHl"
        self.AES_IV = b"ydsecret://query/iv/C@lZe2YzHtZ2CYgaXKSVfsb7Y4QWHjITPPZ0nQp87fBeJ!Iv6v^6fvi2WN@bYpJ4"
        self.key = "asdjnjfenknafdfsdfsd"


    def encrypt_md5(self,str):
        md = hashlib.md5(str.encode('utf-8')).hexdigest()
        return md  # 加密

    def get_secret_key(self):
        url = "https://rlogs.youdao.com/rlog.php"
        session = requests.session()
        session.get(url)
        t = str(int(time.time() * 1000))
        sign = self.get_sign(t,self.key)
        params = {
            "sign": sign,
            "client": "fanyideskweb",
            "product": "webfanyi",
            "appVersion": "1.0.0",
            "vendor": "web",
            "pointParam": "client,mysticTime,product",
            "mysticTime": t,
            "keyfrom": "fanyi.web",
            "keyid": "webfanyi-key-getter",
        }
        res = session.get("https://dict.youdao.com/webtranslate/key", params=params).json()
        secret_key = res["data"]["secretKey"]
        # print(secret_key)
        return secret_key


    def get_sign(self,t,key):
        sign = self.encrypt_md5(f"client={self.l}&mysticTime={t}&product={self.d}&key={key}")
        return sign

    def YouDao_decrypt(self, src: str) -> dict:
        key = self.AES_KEY
        iv = self.AES_IV
        cryptor = AES.new(MD5.new(key).digest()[:16], AES.MODE_CBC, MD5.new(iv).digest()[:16])
        res = cryptor.decrypt(base64.urlsafe_b64decode(src))
        txt = res.decode("utf-8")
        return json.loads(txt[: txt.rindex("}") + 1])

    def translate(self,msg):
        secret_key = self.get_secret_key()
        url = "https://dict.youdao.com/webtranslate"
        headers = {
            "Accept": "application/json, text/plain, */*",
            "Accept-Language": "zh-CN,zh;q=0.9",
            "Connection": "keep-alive",
            "Content-Type": "application/x-www-form-urlencoded",
            'Cookie': 'OUTFOX_SEARCH_USER_ID=-1103603819@10.112.57.88; OUTFOX_SEARCH_USER_ID_NCOO=1058534174.1041499',
            "Origin": "https://fanyi.youdao.com",
            "Referer": "https://fanyi.youdao.com/",
            "Sec-Fetch-Dest": "empty",
            "Sec-Fetch-Mode": "cors",
            "Sec-Fetch-Site": "same-site",
            "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36",
            "sec-ch-ua": "\"Chromium\";v=\"110\", \"Not A(Brand\";v=\"24\", \"Google Chrome\";v=\"110\"",
            "sec-ch-ua-mobile": "?0",
            "sec-ch-ua-platform": "\"macOS\""
        }
        t = str(int(time.time()*1000))
        sign = self.get_sign(t,secret_key)
        data = {
            "i": f"{msg}",
            "from": "auto",
            "to": "",
            "dictResult": "true",
            "keyid": "webfanyi",
            "sign": f"{sign}",
            "client": "fanyideskweb",
            "product": "webfanyi",
            "appVersion": "1.0.0",
            "vendor": "web",
            "pointParam": "client,mysticTime,product",
            "mysticTime": f"{t}",
            "keyfrom": "fanyi.web"
        }
        response = requests.post(url, headers=headers,data=data).text
        # print(response)
        result = self.YouDao_decrypt(response)
        return result


if __name__ == '__main__':
    YouDao = YouDao()
    msg = input(f"请输入需要翻译的单词:")
    result = YouDao.translate(msg)
    print(result)



推荐
梦汐 发表于 2023-4-16 08:18
可高并发的谷歌翻译接口
async function translation(array) {
    var splicing = []
    if (!(array instanceof Array)) {
        array = [array]
    }
    for (let i = 0; i < array.length; i++) {
        splicing.push(
            {
                "originalText": array[i],
                "translatedText": null,
                "detectedLanguage": null,
                "status": "translating",
                "waitTranlate": {}
            }
        )
    }
    return await makeRequest("auto", "zh-CN", splicing)//无并发限制端口
    async function makeRequest(sourceLanguage, targetLanguage, requests) {
        return await new Promise((resolve, reject) => {
            const xhr = new XMLHttpRequest();
            xhr.open(
                "POST",
                "https://translate.googleapis.com/translate_a/t?anno=3&client=te&v=1.0&format=html" + GetExtraParameters(sourceLanguage, targetLanguage, requests)
            );
            xhr.setRequestHeader(
                "Content-Type",
                "application/x-www-form-urlencoded"
            );
            xhr.responseType = "json";

            xhr.onload = (event) => {
                resolve(xhr.response);
            };

            xhr.onerror = xhr.onabort = xhr.ontimeout = (event) => { console.error(event); reject(); };

            xhr.send(getRequestBody(sourceLanguage, targetLanguage, requests));
        });
        function getRequestBody(sourceLanguage, targetLanguage, requests) {
            return requests
                .map((info) => `&q=${encodeURIComponent(info.originalText)}`)
                .join("");
        }
        function GetExtraParameters(sourceLanguage, targetLanguage, requests) {
            return `&sl=${sourceLanguage}&tl=${targetLanguage}&tk=${calcHash(requests.map((info) => info.originalText).join(""))}`
            function calcHash(query) {
                const windowTkk = "448487.932609646";
                const tkkSplited = windowTkk.split(".");
                const tkkIndex = Number(tkkSplited[0]) || 0;
                const tkkKey = Number(tkkSplited[1]) || 0;

                const bytesArray = transformQuery(query);

                let encondingRound = tkkIndex;
                for (const item of bytesArray) {
                    encondingRound += item;
                    encondingRound = shiftLeftOrRightThenSumOrXor(
                        encondingRound,
                        "+-a^+6"
                    );
                }
                encondingRound = shiftLeftOrRightThenSumOrXor(
                    encondingRound,
                    "+-3^+b+-f"
                );

                encondingRound ^= tkkKey;
                if (encondingRound <= 0) {
                    encondingRound = (encondingRound & 2147483647) + 2147483648;
                }

                const normalizedResult = encondingRound % 1000000;
                return normalizedResult.toString() + "." + (normalizedResult ^ tkkIndex);
                function transformQuery(query) {
                    /** @type {Array<number>} */
                    const bytesArray = [];
                    let idx = 0;
                    for (let i = 0; i < query.length; i++) {
                        let charCode = query.charCodeAt(i);

                        if (128 > charCode) {
                            bytesArray[idx++] = charCode;
                        } else {
                            if (2048 > charCode) {
                                bytesArray[idx++] = (charCode >> 6) | 192;
                            } else {
                                if (
                                    55296 == (charCode & 64512) &&
                                    i + 1 < query.length &&
                                    56320 == (query.charCodeAt(i + 1) & 64512)
                                ) {
                                    charCode =
                                        65536 +
                                        ((charCode & 1023) << 10) +
                                        (query.charCodeAt(++i) & 1023);
                                    bytesArray[idx++] = (charCode >> 18) | 240;
                                    bytesArray[idx++] = ((charCode >> 12) & 63) | 128;
                                } else {
                                    bytesArray[idx++] = (charCode >> 12) | 224;
                                }
                                bytesArray[idx++] = ((charCode >> 6) & 63) | 128;
                            }
                            bytesArray[idx++] = (charCode & 63) | 128;
                        }
                    }
                    return bytesArray;
                }
                function shiftLeftOrRightThenSumOrXor(num, optString) {
                    for (let i = 0; i < optString.length - 2; i += 3) {
                        /** @type {string|number} */
                        let acc = optString.charAt(i + 2);
                        if ("a" <= acc) {
                            acc = acc.charCodeAt(0) - 87;
                        } else {
                            acc = Number(acc);
                        }
                        if (optString.charAt(i + 1) == "+") {
                            acc = num >>> acc;
                        } else {
                            acc = num << acc;
                        }
                        if (optString.charAt(i) == "+") {
                            num += acc & 4294967295;
                        } else {
                            num ^= acc;
                        }
                    }
                    return num;
                }
            }
        }
    }
}
console.log(await translation(['hi', 'omg', 'yes']));
沙发
xouou 发表于 2023-4-5 19:55
3#
sorryzzital 发表于 2023-4-5 20:16
我先插个眼,等以后再来好好学习!
4#
ameiz 发表于 2023-4-5 20:25
学习了,谢谢分享
5#
cn2jp 发表于 2023-4-5 20:46
本来想打开moudao的页面对照研究一下,可是为什么打不开呢?
6#
 楼主| hans7 发表于 2023-4-5 20:50 |楼主
cn2jp 发表于 2023-4-5 20:46
本来想打开moudao的页面对照研究一下,可是为什么打不开呢?

当然不能直接点开
7#
Avicii111 发表于 2023-4-5 23:43
学习了,感谢分享!
头像被屏蔽
8#
hgfu566 发表于 2023-4-6 00:05
提示: 该帖被管理员或版主屏蔽
9#
william0402 发表于 2023-4-6 07:54
学习了,感谢分享
10#
wuai4444 发表于 2023-4-6 09:02
感谢分享
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2024-12-30 23:34

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表