美文网首页
网易云音乐APP——接口加密算法

网易云音乐APP——接口加密算法

作者: 懒树趾三 | 来源:发表于2019-05-15 18:59 被阅读0次

    延续上一篇的文章,今天我想聊聊网易云音乐的接口加密算法(先打开js的项目工程,谢谢https://github.com/Binaryify/NeteaseCloudMusicApi)

    随便搜了一下代码,比如login,基本能锁定两个比较重要的文件,request.js和crypto.js

    crytpo.js用来封装加密算法,也是请求部分的核心之一,我要做的其实很简单,用Java实现里面js功能。
    下面是js的登录代码封装

    // 手机登录
    
    const crypto = require('crypto')
    
    module.exports = (query, request) => {
        query.cookie.os = 'pc'
        const data = {
            phone: query.phone,
            countrycode: query.countrycode,
            password: crypto.createHash('md5').update(query.password).digest('hex'),
            rememberLogin: 'true'
        }
        console.log(data);
        return request(
            'POST', `https://music.163.com/weapi/login/cellphone`, data,
            {crypto: 'weapi', ua: 'pc', cookie: query.cookie, proxy: query.proxy}
        )
    }
    
    

    要注意几个细节:
    1、query.cookie.os = 'pc',在Java里面其实就是在请求的cookie里面加上"oc","pc"(用hashmap设置到"Cookie"里面,然后一起封装到header里面)
    2、对密码做MD5加密,这个没啥难度,通用加密方式,如果你要偷懒也行

    package music.netease.com.neteasecloudmusic.utils;
    
    import java.security.MessageDigest;
    
    public class MD5Utils {
        public final static String MD5(String s) {
            char hexDigits[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
                    'a', 'b', 'c', 'd', 'e', 'f' };
            try {
                byte[] btInput = s.getBytes();
                // 获得MD5摘要算法的 MessageDigest 对象
                MessageDigest mdInst = MessageDigest.getInstance("MD5");
                // 使用指定的字节更新摘要
                mdInst.update(btInput);
                // 获得密文
                byte[] md = mdInst.digest();
                // 把密文转换成十六进制的字符串形式
                int j = md.length;
                char str[] = new char[j * 2];
                int k = 0;
                for (int i = 0; i < j; i++) {
                    byte byte0 = md[i];
                    str[k++] = hexDigits[byte0 >>> 4 & 0xf];
                    str[k++] = hexDigits[byte0 & 0xf];
                }
                return new String(str);
            } catch (Exception e) {
                return null;
            }
        }
    }
    
    

    MD5加密后的结果最好验证一下,对比一下js和Java加密后的打印数据

    3、接着可以直接跳到request.js那个问题,里面封装了最终的网络请求,debug或者log能很快定位到参数处理的位置,当然,如果代码感觉好可以直接看到

    
        if (options.crypto == 'weapi') {
          let csrfToken = (headers['Cookie'] || '').match(/_csrf=([^(;|$)]+)/)
    
          console.log(csrfToken);
    
          data.csrf_token = csrfToken ? csrfToken[1] : ''
          data = encrypt.weapi(data)
          console.log('weapi:'+queryString.stringify(data));
          url = url.replace(/\w*api/, 'weapi')
    
        } else if (options.crypto == 'linuxapi') {
          data = encrypt.linuxapi({
            method: method,
            url: url.replace(/\w*api/, 'api'),
            params: data
          })
          
          headers['User-Agent'] =
            'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36'
          url = 'https://music.163.com/api/linux/forward'
        }
    
    

    登陆请求的option.crypto是weapi,请求的参数都会以键值对的方式放到map中,然后转成jsonObject调用Encrypt,看return就知道最终返回的还是个map

    js:
    function Encrypt(obj) {
      var text = JSON.stringify(obj);
      console.log(text);
      var secKey = createSecretKey(16)
      console.log(secKey);
      var encText = aesEncrypt(aesEncrypt(text, nonce), secKey);
      var encSecKey = rsaEncrypt(secKey, pubKey, modulus);
    
      return {
        params: encText,
        encSecKey: encSecKey
      }
    }
    
    对应的Java:
      public static Map<String, String> encrypt(String text) {
            String secKey = getRandomString(16);
    //        String encText = aesEncrypt(aesEncrypt(text, nonce), secKey);
            String encText = aesEncrypt(aesEncrypt(text, nonce), secKey);
            String encSecKey = rsaEncrypt(secKey, pubKey, modulus);
    
            Map<String, String> map = new HashMap<String, String>();
            map.put(PARAMS, encText);
            map.put(ENCSECKEY, encSecKey);
            return map;
        }
    

    这部分的代码大致意思应该是,把传进来的参数转成字符串赋值给text,Java我就用string,randomByte(16)就很简单了,获取一个16位的随机数(从26个字母大小写+0~9这10个数字),赋值给secretKey,然后再这两个参数做两次aes加密,一次rsa加密。
    随机数的处理我在Java里面单独写了一个方法,不知道为啥js可以这么简洁,我一定要找时间学一下

      /*获取由字母和数字组成的随机数*/
        static private String getRandomString(int length){
            String str="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
            Random random=new Random();
            StringBuffer sb=new StringBuffer();
            for(int i=0;i<length;i++){
                int number=random.nextInt(62);
                sb.append(str.charAt(number));
            }
            return sb.toString();
        }
    

    str对应了js代码里面的base62那个变量,aes加密需要传入text和seckey

    js:
    function aesEncrypt(text, secKey) {
      var _text = text;
      var lv = new Buffer('0102030405060708', "binary");
      var _secKey = new Buffer(secKey, "binary");
      var cipher = crypto.createCipheriv('AES-128-CBC', _secKey, lv);
      var encrypted = cipher.update(_text, 'utf8', 'base64');
      encrypted += cipher.final('base64');
      return encrypted;
    }
    java:
        private static String aesEncrypt(String text,String mode,String key,IvParameterSpec iv){
            try {
                SecretKeySpec skeySpec = new SecretKeySpec(key.getBytes("UTF-8"), "AES");
    
                Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
                cipher.init(Cipher.ENCRYPT_MODE, skeySpec, iv);
    
                byte[] encrypted = cipher.doFinal(text.getBytes());
    
                return Base64Encoder.encode(encrypted);
            } catch (Exception e) {
                return "";
            }
    
        }
    

    没啥好说的,就按照原装的写法,百度,弄出一个类似的表达式,把参数对应上,有一个关键的字段nonce,应该是秘钥,第一次aes加密用text(也就是参数转化的json)和nonce,再将加密后的结果和那个16位的随机数(随机秘钥)穿进去生成最终的encText。最终的encSecKey通过rsa加密实现

    js:
    function rsaEncrypt(text, pubKey, modulus) {
      var _text = text.split('').reverse().join('');
      var biText = bigInt(new Buffer(_text).toString('hex'), 16),
          biEx = bigInt(pubKey, 16),
          biMod = bigInt(modulus, 16),
          biRet = biText.modPow(biEx, biMod);
      return zfill(biRet.toString(16), 256);
    }
    
    java:
       private static String rsaEncrypt(String text, String pubKey, String modulus) {
            text = new StringBuilder(text).reverse().toString();
            BigInteger rs = new BigInteger(String.format("%x", new BigInteger(1, text.getBytes())), 16)
                    .modPow(new BigInteger(pubKey, 16), new BigInteger(modulus, 16));
            String r = rs.toString(16);
            if (r.length() >= 256) {
                return r.substring(r.length() - 256, r.length());
            } else {
                while (r.length() < 256) {
                    r = 0 + r;
                }
                return r;
            }
        }
    

    text就是那个16位的随机数,pubkey和modulus是抓包拿到的,js文件里面有。最后将两个参数封装到map里面返回出去。
    不要看到js代码的接口都是get,其实那是因为大神在底层已经封装了一层,所有的接口都能用post的方式实现,还有就是很多接口的请求地址并非是大神提供的那些,你要好好接口文档,或者学我直接调试看日志。加密后的map最后转化成formbody,然后用post的方式去请求,完工!至于大神提到的很多head cookies,我跑了一下Java控制台debug了一下,基本都不用带。对于js加密转Java加密,我就介绍到这里,有啥不理解的欢迎评论留言,Android版本的网易云音乐包括登录只实现了5个接口,获取用户歌单、歌单的歌曲列表、收藏的MV,视频播放链接获取。然后加上了ijkPlayer库,实现基本的视频播放功能,由于页面太low,功能太少就暂时不开放了,争取月底完成音乐播放功能,到时候将所有代码开放到GitHub上,需要技术交流的话可以私下沟通。

    相关文章

      网友评论

          本文标题:网易云音乐APP——接口加密算法

          本文链接:https://www.haomeiwen.com/subject/tflmaqtx.html