解析网易云音乐的加密方式

写在前面

前段时间使用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分析:
  1. 函数a可以看出返回的是一个长度为16的随机字符串

  2. 函数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

参考接口

node版本(史上最全)

最后奉上源码:

如果此项目对你有所帮助,麻烦给****Star****吧。感谢!!!(本项目仅供学习参考,侵权删)

推荐阅读更多精彩内容