解析网易云音乐的加密方式
写在前面
前段时间使用VSCode时,看到一堆神奇的插件,其中包括VSC Netease Music,经研究发现该作者参考的是node版本接口实现,本着对技术的渴望,研究一波加密方式,并复写Java版本随手记录一下。
准备工作
- 环境:Win10
- 工具:Fiddler 4,Chrome浏览器
以音乐歌曲评论数据获取接口为例,进行分析
一、查找API
打开歌曲详情页面,F12打开DevTools工具页面,找到接口如下
查看该请求的详细内容,request Header如下所示:
我们分析一下这个请求,先看它的url,请求多次之后发现R_SO_4_在请求评论时是固定的,1377544581则是歌曲的id,url还有一个参数csrf_token,看这个名字像是防止跨站攻击的,但是它一直是空的。然后就是POST里面的参数params和encSecKey,这两个参数是关键,接下来我们要重点分析它。
从当前的值可以看出,这是加密后的内容,毫无疑问肯定是通过js加密的。而且,我们可以从上图的Initiator可以看出这两个参数是通过core.js这个js文件算出来。因此,我们下一步计划就是分析core.js的内容。
二、分析Core.js
文件另存下来后查看是压缩过的,需要格式化后大概四万多行。但是没关系,我们需要的只是部分数据。
在这个js文件中搜索params和encSecKey,可以找到这里
问题就变成得到这个bXY6S,它是由window.asrsea这个函数得到的,可以看到有4个参数,如果研究每个参数肯定是痛苦的,也没有必要,可以先把它们输出来看一下,使用Fiddler线上调试js,原理就是将本地的js替换线上加载的js文件,这样就可以调试输出这4个参数值。本地js文件加上几行代码,如图所示:
打开Fiddler,找到autoResponder,添加Rule,导入本地js文件最终页面如下图所示:
然后就成功找到了i0x,如图
可以根据不同的歌曲和翻译页数多试几次,可以发现rid就是R_SO_4_加上歌曲的id(其实这个参数也是可以没有的),offset就是(评论页数-1) * 20,total在第一页是true,其余是false。
按这样的方式可以得到其余三个参数
- 010001
- 00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7
- 0CoJUm6Qyw8W8jud
三、加密方式分析
现在我们只要知道函数window.asrsea如何处理的就可以了,定位到这个函数发现它其实是一个叫d的函数
(1)params分析:
-
函数a可以看出返回的是一个长度为16的随机字符串
-
函数b是一个AES加密,经过了两次加密,第一次对d也就是那个json加密,key是第四个参数,第二次对第一次加密结果进行加密,key是i。在b函数中可以看到密钥偏移量iv是0102030405060708,模式是CBC
(2)encSecKey分析
你会发现在我们这种情境下,这里传入c的三个参数i是16个F,e是第二个参数,f是第三个参数,全部是固定的值,那么无论歌曲id或评论页数如何变化,这个encSecKey都不随之发生变化,所以这个encSecKey对我们来说就是个常量,抄一个下来就是可以使用的。
秉承着完美解决,我实在不想写死,接着再分析
这个参数通过RSA算法生成,其中i作为message,e,f是加密时用到的参数。
在这里稍微解释一下RSA算法,算法选取2个很大的质数p,q,得到它们的乘积n,然后选取e,d满足e*d = 1 mod (p-1)(q-1),加密时text=(msge)%n,解密时msg=(textd)%n,在这个函数里e就相当于算法里的e,f相当于算法里的n。
还有一点需要注意,encSecKey是一个完全由16进制数组成,但是在加密模块中一般都是返回byte流,然后通过base64编码(长度是原来的4/3),而像这种的应该是把byte流通过16进制表示出来(长度是原来的2倍)。
这里面有个小坑(当时懵逼很久)
通过代码可以看出,c数组是b字符串转成的数组,然后在for循环中,c数组从左到右是从低位加到高位的,比如123456,1是加在低位,6是加在高位,这和平常有些不一样。
即需要先将加密的消息翻转,再进行加密
四、最终实现(Java版)
核心加密如下
/**
* AES加密
* 此处使用AES-128-CBC加密模式,key需要为16位
*
* @param content 加密内容
* @param sKey 偏移量
* @return
*/
public static String aesEncrypt(String content, String sKey) throws Exception {
byte[] encryptedBytes;
byte[] byteContent = content.getBytes("UTF-8");
// 获取cipher对象,getInstance("算法/工作模式/填充模式")
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
// 采用AES方式将密码转化成密钥
SecretKeySpec secretKeySpec = new SecretKeySpec(sKey.getBytes(), "AES");
// 初始化偏移量
IvParameterSpec iv = new IvParameterSpec(ivParameter.getBytes());
// cipher对象初始化 init(“加密/解密,密钥,偏移量”)
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, iv);
// 数据处理
encryptedBytes = cipher.doFinal(byteContent);
// 此处使用BASE64做转码功能,同时能起到2次加密的作用
return new String(Base64Utils.encode(encryptedBytes), "UTF-8");
}
/**
* RSA 加密
*
* @param secKey 随机16位字符串
* @return
*/
public static String rsaEncrypt(String secKey) {
// 需要先将加密的消息翻转,再进行加密
secKey = new StringBuffer(secKey).reverse().toString();
// 转十六进制字符串
String secKeyHex = stringToHexString(secKey);
// 指定基数的数值字符串转换为BigInteger表示形式
BigInteger biText = new BigInteger(secKeyHex, 16);
BigInteger biEx = new BigInteger(pubKey, 16);
BigInteger biMod = new BigInteger(modulus, 16);
// 次方并求余(biText^biEx mod biMod is ?)
BigInteger bigInteger = biText.modPow(biEx, biMod);
return zfill(bigInteger.toString(16), 256);
}
五、这哥们更会玩
总是有那么些大牛平时没事干就喜欢琢磨这些事情,通过破解这些程序来证明自己。还有的是为了喜欢的女孩,比如下面这位:(这是一个悲伤的故事!)
这位同学的代码分析能力很强,他提供的方法属于另辟蹊径。其他的大牛都是通过分析js加密算法,然后自己写出来,实现对传输参数的加密,大部分都是使用Python,这位作者使用的是纯Java写的加密程序。通过java内置的ScriptEngine调用js引擎,实现对js中的方法调用,这个我也是第一次听说,在JavaSE6中提供的功能。什么是ScriptEngine,请看博客:https://www.cnblogs.com/zouhao/p/3644788.html或者
http://blog.csdn.net/u012660667/article/details/49821811
作者通过对core.js的核心文件分析,将两万行的代码删减成一千多行,不得不说作者很有耐心啊!最后就简单了,直接在java代码中调用js的方法就可以对参数进行加密了。
public class JSSecret {
private static Invocable inv;
public static final String encText = "encText";
public static final String encSecKey = "encSecKey";
/**
* 从本地加载修改后的 js 文件到 scriptEngine
*/
static {
try {
// 文件读取
String pathResources = ResourceUtils.getURL("classpath:").getPath();
pathResources = pathResources + "file/core.js";
pathResources = pathResources.substring(1, pathResources.length());
Path path = Paths.get(pathResources);
byte[] bytes = Files.readAllBytes(path);
String js = new String(bytes);
ScriptEngineManager factory = new ScriptEngineManager();
// 查找并创建一个ScriptEngine
ScriptEngine engine = factory.getEngineByName("JavaScript");
// js代码放入到eval中当做参数就可以执行相应的js代码
engine.eval(js);
// 调用js中的方法
inv = (Invocable) engine;
System.out.println("Init completed");
} catch (Exception e) {
e.printStackTrace();
}
}
public static ScriptObjectMirror get_params(String paras) throws Exception {
ScriptObjectMirror so = (ScriptObjectMirror) inv.invokeFunction("myFunc", paras);
return so;
}
public static HashMap<String, String> getData(String paras) {
try {
ScriptObjectMirror so = (ScriptObjectMirror) inv.invokeFunction("myFunc", paras);
Set<Map.Entry<String, Object>> entries = so.entrySet();
for (Map.Entry<String, Object> map : entries) {
System.out.println("key:" + map.getKey());
System.out.println("value:" + map.getValue());
}
HashMap<String, String> data = new HashMap<>();
data.put("params", so.get(JSSecret.encText).toString());
data.put("encSecKey", so.get(JSSecret.encSecKey).toString());
return data;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
详细分析见:https://blog.csdn.net/qq_31673689/article/details/78615448
写在最后
Github已有较为完善的node版本及各种分析文章,但分析尝试并不是很顺利,还是要多学点东西,生命不息,折腾不止!!!
参考文章
ever_hu
平胸小仙女
我是你妹她哥
Mi_Chong
darknessomi
TheAlgorithms
参考接口
最后奉上源码:
- Java加密
- ScriptEngine调用js引擎
- 附上springboot实现的网易云音乐API感兴趣至基于SpringBoot网易云音乐API
- 附上网易云音乐API具体实现感兴趣至仿网易云音乐微信小程序