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

延续上一篇的文章,今天我想聊聊网易云音乐的接口加密算法(先打开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上,需要技术交流的话可以私下沟通。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容