实现目的
有没有过这样的体验?手里攥着几十上百个网盘分享链接,要一个个点进页面、手动确认转存到自己的网盘——重复的操作像流水线作业,不仅浪费时间,而且还可能会漏掉文件。所以搞个自动转存脚本是非常有必要的。
打开分享链接->点击F12->查看网络,
网盘的分享链接数据居然全是加密的。观察了一下加密字符好像是base64,直接一手解码,乱码!!!作为一个对逆向工程一窍不通的开发者,这已经超出了我的能力范围。
既然自己搞不定,不如试试“AI”。刚好试试最近热门的Gork.我把收集到的几条请求体、响应体密文整理好,清晰标注了获取场景,然后一股脑发给了Gork。
让我惊喜的是,Gork不仅精准识别出这些密文来自哪个网盘站点,还直接给出了对应的加密算法类型。我拿着密钥代入算法解密,结果发现解密失败
。
我只能继续问Gork,让它提供判断加密算法和密钥的参考来源。好在Gork很配合,直接给出了几个链接,包含GitHub及CSDN的链接。将几个站点都查看了一遍,选择了最简单的,文件名清晰且代码精简。
经过测试,解密可以使用,但是加密报错。将问题再扔给Gork,修复了即便终于给了我可以使用的代码,自己再整合一下就完成了转存分享中最难的部分
。
演示链接
链接: https://yun.139.com/shareweb/#/w/i/2qidG1XEkUKi0 提取码:d78q 复制内容打开中国移动云盘手机APP,操作更方便哦
解密解密分析
| 项目 | 说明 |
|---|---|
| 算法 | AES-128-CBC + PKCS7 填充 |
| 密钥 | "PVGDwmcvfs1uV3d1"(UTF-8 编码) |
| IV | 随机,前 16 字节拼接在密文前 |
| 密文 | Base64 解码后第 17 字节起 |
| 编码 | 整体 Base64(标准,无 URL-safe) |
加解密代码
需要安装crypto-js库
const CryptoJS = require("crypto-js");
// 新版外链接口固定密钥
const FIXED_KEY = "PVGDwmcvfs1uV3d1";
/**
* 移动云盘新版外链加密(OutLink)
* @param {string} plaintextJson 明文JSON字符串
* @returns {string} 最终的Base64密文(和官方App一模一样)
*/
function encryptOutLink(plaintextJson) {
// 1. 生成随机IV(16字节)
const iv = CryptoJS.lib.WordArray.random(16);
// 2. AES-128-CBC 加密
const encrypted = CryptoJS.AES.encrypt(
plaintextJson,
CryptoJS.enc.Utf8.parse(FIXED_KEY),
{
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
}
);
// 3. 把 IV(16字节) + 密文 拼接起来
const ivAndCiphertext = iv.concat(encrypted.ciphertext);
// 4. 整体转Base64 → 这就是最终发出去的字符串
return ivAndCiphertext.toString(CryptoJS.enc.Base64);
}
/**
* 解密移动云盘新版外链(OutLink)
* @param {string} encryptedBase64 响应体的 Base64 字符串(可能带空格)
* @returns {string} 解密后的明文 JSON
*/
function decryptOutLink(encryptedBase64) {
try {
// 1. 清理 Base64
const cleanB64 = encryptedBase64.replace(/\s+/g, '');;
// 2. Base64 解码 → WordArray
const combined = CryptoJS.enc.Base64.parse(cleanB64);
const totalLength = combined.sigBytes; // 总字节数
// 3. 提取 IV(前 16 字节 = 4 words)和密文(剩余)
const ivBytes = combined.sigBytes / 4; // 每个 word 4 字节
const iv = CryptoJS.lib.WordArray.create(combined.words.slice(0, 4)); // 前 16 字节
const ciphertext = CryptoJS.lib.WordArray.create(
combined.words.slice(4),
totalLength - 16 // 剩余长度
);
// 4. AES-128-CBC 解密
const decrypted = CryptoJS.AES.decrypt(
{ ciphertext: ciphertext },
CryptoJS.enc.Utf8.parse(FIXED_KEY),
{
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
}
);
// 5. 转 UTF-8 字符串(JSON)
const plaintext = decrypted.toString(CryptoJS.enc.Utf8);
if (!plaintext) {
throw new Error("解密为空,可能是填充错误或密钥不对");
}
return plaintext;
} catch (error) {
throw new Error(`解密失败: ${error.message}`);
}
}
// 测试
// getOutLinkGeneral 请求体加密
const test1 = JSON.stringify({
getOutLinkGeneralReq: {
linkID: "2qidFfUiXYAas",
isPasswd: 1
}
});
const res1 = encryptOutLink(test1);
console.log("getOutLinkGeneral 请求体加密结果:", res1);
console.log("getOutLinkGeneral 请求体解密结果:", decryptOutLink(res1));
// getOutLinkInfoV6 请求体加密
const test2 = JSON.stringify({
getOutLinkInfoReq: {
account: "",
linkID: "2qidG1XEkUKi0",
passwd: "d78q",
caSrt: 0,
coSrt: 0,
srtDr: 1,
bNum: 1,
pCaID: "root",
eNum: 200
}
});
const res2 = encryptOutLink(test2);
console.log("getOutLinkInfoV6 请求体加密结果:", res2);
console.log("getOutLinkInfoV6 请求体解密结果:", decryptOutLink(res2));
// getOutLinkGeneral 响应体解密
const test3 = `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`;
const res3 = decryptOutLink(test3);
console.log("getOutLinkGeneral 响应体解密结果:", res3);
console.log("getOutLinkGeneral 响应体加密结果:", encryptOutLink(res3));
// getOutLinkInfoV6 响应体解密
const text4 = "q1FepUXZdRCJESkrGVyAkMsyT+9 NJNaPxqtbSHUvoMsRyKS2Ju2Ed8nQR/XmD9c1hlRRRpMtR++bsEbNRKL6Z9dvrqioStjUN3Cfi41ANo8YDbXvqvVfIosSGWym73S7+UxadvzDsLFwx6sgAOxMWhoyY2Lkcmt9YkI6oNAYmf5AZUgUhl/oSuXg4PJYOk1MimRv5etgqpTvwMQwQHRsZt4FPOlSLq6bL3R721PUQ3049/UBrom92I0FYGQJ6kjP1I7fRrC1yj2RPiqtVTOv3dsldvtaDj0j9dxkMhBiW3kwzN4cAMld60Q3llLEuaNcwmMANNeKjaeyEpXUUgtDgEIrL9+kSC/Qpkll3rh27uF3lUG4eLB5Ij0a6ABG+zHSau3gGM8/vrFWKOigdAlq2zMaa/Zp3hIp2rBEAKRt+YIR4hC9BnlxrP8d9Wm7vJB/EGWWiHb2kkiULpI5DFToX1w/8 jSvr3XuGtg9JopuRbK4Z6HJYRaJ9D+45 yHyYurSucqzakgDJER1WpfM3UZNcBvD2xgkbUb0vUdiVCvqz1E+/0I+OEzVb3p/w6ARI3SHENQGLTvdCkpW9HmAIWDwz8ONl6v2/8 yqAZFVpwJYmfqrYjUyWXmZZwsoNBgE5fg+SE2nkb90I7frKqRNiRfu7+V0NuL5T/dHHCaEXxuVxR3dm7h9hDmh/9 fnTWBTSIld4sKBmQY8wJNDziy0guKNOVO1TpSAcCWXUcpc913s1ycVWPY6eGfohZUrMu0C3WmuxGQEVHD3EPaU17fBCmxaGLm5SqaNY/g+P0kGNw0SZFybo/K6rk3c24M2zrSR6YiN2TGc+9 YpDDnV0PUH0vfcgThw8EPpmx0pJ4AgfqsbxYXOwo3OpMkiVZWjkfUj2yZiRypd+RybH7iJpd141kp8Y+H4r/o47GJ8D72D74f3AI25lJz0jtdnRE/UHJR9VAIRL9gTYVIxupObrJUcXbVEZ1sCEVx9NyZssoUiaTwDNazaLqHnmelCaqoetNEdWIt8"
const res4 = decryptOutLink(text4);
console.log("getOutLinkInfoV6 响应体解密结果:", res4);
console.log("getOutLinkInfoV6 响应体加密结果:", encryptOutLink(res4));