applicationInfoList = packageManager
.getInstalledApplications(PackageManager.GET_META_DATA);
for (ApplicationInfo item : applicationInfoList) {
if (item.packageName.equals(XPOSED_INSTALLER)) {
return true;
}
}
return false;
}
/**
* 第三种:通过ClassLoader检查是否已经加载了XposedBridge类和XposedHelpers类来检测
*
* @return
*/
@Deprecated
public boolean isXposedExists() {
try {
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
//调用 loadClass 加载类
Class> aClass = systemClassLoader.loadClass(XPOSED_HELPERS);
Object xpHelperObj = aClass.newInstance();
} catch (InstantiationException e) {
e.printStackTrace();
return true;
} catch (IllegalAccessException e) {
e.printStackTrace();
return true;
} catch (ClassNotFoundException e) {
e.printStackTrace();
return false;
}
try {
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
//调用 loadClass 加载类
Class> aClass = systemClassLoader.loadClass(XPOSED_BRIDGE);
Object xpBridgeObj = aClass.newInstance();
} catch (InstantiationException e) {
e.printStackTrace();
return true;
} catch (IllegalAccessException e) {
e.printStackTrace();
return true;
} catch (ClassNotFoundException e) {
e.printStackTrace();
return false;
}
return true;
}
/**
* 第二种:通过主动抛出异常,检查堆栈信息来判断是否存在XP框架
*
* @return
*/
public static boolean isXposedExistByThrow() {
try {
throw new Exception("gg");
} catch (Throwable e) {
return isXposedExists(e);
}
}
/**
* 第二种:判断是否有Xposed环境
*
* @param thr 异常
* @return
*/
public static boolean isXposedExists(Throwable thr) {
StackTraceElement[] stackTraces = thr.getStackTrace();
for (StackTraceElement stackTrace : stackTraces) {
final String clazzName = stackTrace.getClassName();
if (clazzName != null && clazzName.contains(XPOSED_BRIDGE)) {
return true;
}
}
return false;
}
/**
* 第三种:尝试关闭XP框架
* 先通过isXposedExistByThrow判断有没有XP框架
* 有的话先hookXP框架的全局变量disableHooks
*
* 漏洞在,如果XP框架先hook了isXposedExistByThrow的返回值,那么后续就没法走了
* 现在直接先hookXP框架的全局变量disableHooks
*
* 1.如果不想自己的APP被Xposed框架修改,可以在应用内部关闭Xposed框架Hook的总开关,使其无法对应用程序进行Hook。
* 2.这个所谓的“总开关”实质上为XposedBridge.java文件中的disableHooks变量
* 3.disableHooks变量默认为false,若该值为true则表示关闭了Xposed框架Hook的总开关。
* 4.可以通过反射的方法去获取disableHooks变量
*
* @return 是否关闭成功的结果
*/
public static boolean tryShutdownXposed() {
Field xpdisabledHooks;
try {
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
Class> aClass = systemClassLoader.loadClass(XPOSED_BRIDGE);
xpdisabledHooks = aClass.getDeclaredField("disableHooks");
xpdisabledHooks.setAccessible(true);
xpdisabledHooks.set(null, Boolean.TRUE);
return true;
} catch (NoSuchFieldException e) {
e.printStackTrace();
return false;
} catch (ClassNotFoundException e) {
e.printStackTrace();
return false;
} catch (IllegalAccessException e) {
e.printStackTrace();
return false;
}
}
/**
* 第四种:获取DEX加载列表,判断其中是否包含XposedBridge.jar等字符串。
*
* @return
*/
public static boolean isXposedByJar() {
boolean z = false;
try {
ClassLoader classLoader = ClassLoader.getSystemClassLoader();
Class> cls = Class.forName("dalvik.system.DexPathList");
Method method = Class.forName("dalvik.system.DexPathList$Element").getMethod("toString", new Class[0]);
Field declaredField = cls.getDeclaredField("dexElements");
declaredField.setAccessible(true);
Field declaredField2 = BaseDexClassLoader.class.getDeclaredField("pathList");
declaredField2.setAccessible(true);
Object[] objArr = (Object[]) declaredField.get(declaredField2.get(classLoader));
for (Object obj : objArr) {
try {
String str2 = (String) method.invoke(obj, new Object[0]);
if (str2 != null && str2.contains("XposedBridge.jar")) {
z = true;
}
} catch (Exception e2) {
z = false;
}
}
}catch(Exception e3){
z = false;
}
return z;
}
}
================================================
FILE: NotCaptureLib/src/main/res/xml/network_security_config.xml
================================================
================================================
FILE: NotCaptureLib/src/main/res/xml/network_security_config_debug.xml
================================================
================================================
FILE: README.md
================================================
#### 目录介绍
- 01.整体概述介绍
- 1.1 项目背景
- 1.2 思考问题
- 1.3 设计目标
- 1.4 收益分析
- 02.市面抓包的分析
- 2.1 Https三要素
- 2.2 抓包核心原理
- 2.3 搞定CA证书
- 2.4 突破CA证书校验
- 2.5 如何搞定加解密
- 2.6 Charles原理
- 2.7 抓包原理图
- 2.8 抓包核心流程
- 03.防止抓包思路
- 3.1 先看如何抓包
- 3.2 设置配置文件
- 3.3 数据加密处理
- 3.4 避免黑科技抓包
- 04.防抓包实践开发
- 4.1 App安全配置
- 4.2 关闭代理
- 4.3 证书校验
- 4.4 双向认证
- 4.5 防止挂载抓包
- 4.6 数据加解密
- 4.7 证书锁定
- 4.8 Sign签名
- 4.9 其他的方式
- 05.架构设计说明
- 5.1 整体架构设计
- 5.2 关键流程图
- 5.3 稳定性设计
- 5.4 降级设计
- 5.5 异常设计说明
- 06.防抓包功能自测
- 6.1 网络请求测试
- 6.2 抓包测试
- 6.3 黑科技挂载测试
- 6.4 逆向破解测试
### 01.整体概述介绍
#### 1.1 项目背景
- 通讯安全是App安全检测过程中非常重要的一项
- 针对该项的主要检测手段就是使用中间人代理机制对网络传输数据进行抓包、拦截和篡改,以检验App在核心链路上是否有安全漏洞。
- 保证数据安全
- 通过charles等工具可以对app的网络请求进行抓包,这样这些信息就会被清除的提取出来,会被不法分子进行利用。
- 不想被竞争对手逆向抓包
- 不想自身App的数据被别人轻而易举地抓包获取到,从而进行类似业务或数据分析、爬虫或网络攻击等破坏性行为。
#### 1.2 思考问题
- 开发项目的时候,都需要抓包,很多情况下即使是Https也能正常抓包正常。那么问题来了:
- 抓包的原理是?任何Https的 app 都能抓的到吗?如果不能,哪些情况下可以抓取,哪些情况下抓取不到?
- 什么叫做中间人攻击?
- 使用HTTPS协议进行通信时,客户端需要对服务器身份进行完整性校验,以确认服务器是真实合法的目标服务器。
- 如果没有校验,客户端可能与仿冒的服务器建立通信链接,即“中间人攻击”。
#### 1.3 设计目标
- 防止App被各种方式抓包
- 做好各种防抓包安全措施,避免各种黑科技抓包。
- 沉淀为技术库复用
- 目前只是针对App端有需要做防抓包措施,后期其他业务线可能也有这个需要。因此下沉为工具库,傻瓜式调用很有必要。
- 该库终极设计目标如下所示
- 第一点:必须是低入侵性,对原有代码改动少,最简单的加入是一行代码设置即可。完全解耦合。
- 第二点:可以动态灵活配置,支持配置禁止代理,支持配置是否证书校验,支持配置域名合法性过滤,支持拦截器加解密数据。
- 第三点:可以检测App是否在双开,挂载,Xposed攻击环境
- 第四点:可以灵活设置加解密的key,可以灵活替换加解密方式,比如目前采用RC4,另一个项目想用DES,可以灵活更换。
#### 1.4 收益分析
- 抓包库收益
- 提高产品App的数据安全,必须对数据传输做好安全保护措施和完整性校验,以防止自身数据在网络传输中裸奔,甚至是被三方恶意利用或攻击。
- 技能的收益
- 下沉为功能基础库,可以方便各个产品线使用,提高开发的效率。避免跟业务解耦合。傻瓜式调用,低成本接入!
### 02.市面抓包的分析
#### 2.1 Https三要素
- 要清楚HTTPS抓包的原理,首先需要先说清楚 HTTPS 实现数据安全传输的工作原理,主要分为三要素和三阶段。
- Http传输数据目前存在的问题
- 1.通信使用明文,内容可能被窃听;2.不验证通信方的身份,因此可能遭遇伪装;3.无法证明报文的完整性,所以有可能遭到篡改。
- 
- Https三要素分别是:
- 1.加密:通过对称加密算法实现。
- 2.认证:通过数字签名实现。(因为私钥只有 “合法的发送方” 持有,其他人伪造的数字签名无法通过验证)
- 3.报文完整性:通过数字签名实现。(因为数字签名中使用了消息摘要,其他人篡改的消息无法通过验证)
- Https三阶段分别是:
- 1.CA 证书校验:CA 证书校验发生在 TLS 的前两次握手,客户端和服务端通过报文获得服务端 CA 证书,客户端验证 CA 证书合法性,从而确认 CA 证书中的公钥合法性(大多数场景不会做双向认证,即服务端不会认证客户端合法性,这里先不考虑)。
- 2.密钥协商:密钥协商发生在 TLS 的后两次握手,客户端和服务端分别基于公钥和私钥进行非对称加密通信,协商获得 Master Secret 对称加密私钥(不同算法的协商过程细节略有不同)。
- 3.数据传输:数据传输发生在 TLS 握手之后,客户端和服务端基于协商的对称密钥进行对称加密通信。
- Https流程图如下
- 
#### 2.2 抓包核心原理
- HTTPS抓包原理
- Fiddler、Charles等抓包工具,其实都是采用了中间人攻击的方案: 将客户端的网络流量代理到MITM(中间人)主机,再通过一系列的面板或工具将网络请求结构化地呈现出来。
- 抓包Https有两个突破点
- CA证书校验是否合法;数据传递过程中的加密和解密。如果是要抓包,则需要突破这两点的技术,无非就是MITM(中间人)伪造证书和使用自己的加解密方式。
- 抓包的工作流程如下
- 中间人截获客户端向发起的HTTPS请求,佯装客户端,向真实的服务器发起请求;
- 中间人截获真实服务器的返回,佯装真实服务器,向客户端发送数据;
- 中间人获取了用来加密服务器公钥的非对称秘钥和用来加密数据的对称秘钥,处理数据加解密。
#### 2.3 搞定CA证书
- Https抓包核心CA证书
- HTTPS抓包的原理还是挺简单的,简单来说,就是Charles作为“中间人代理”,拿到了服务器证书公钥和HTTPS连接的对称密钥。
- 前提是客户端选择信任并安装Charles的CA证书,否则客户端就会“报警”并中止连接。这样看来,HTTPS还是很安全的。
- 安装CA证书到手机中必须洗白
- 抓包应用内置的 CA 证书要洗白,必须安装到系统中。而 Android 系统将 CA 证书又分为两种:用户 CA 证书和系统 CA 证书(必要Root权限)。
- Android从7.0开始限制CA证书
- 只有系统(system)证书才会被信任。用户(user)导入的Charles根证书是不被信任的。相当于可以理解Android系统增加了安全校验!
- 如何绕过CA证书这种限制呢?已知有以下四种方式
- 第一种方式:AndroidManifest 中配置 networkSecurityConfig,App 信任用户 CA 证书,让系统对用户 CA 证书的校验给予通过。
- 第二种方式:调低 targetSdkVersion < 24,不过这种方式谷歌市场有限制,意味着抓 HTTPS 的包越来越难操作。
- 第三种方式:挂载App抓包,VirtualApp 这种多开应用可以作为宿主系统来运行其它应用,利用xposed避开CA证书校验。
- 第四种方式:Root手机,把 CA 证书安装到系统 CA 证书目录中,那这个假 CA 证书就是真正洗白了,难度较大。
#### 2.4 突破CA证书校验
- App版本如何让证书校验安全
- 1.设置targetSdkVersion大于24,去掉清单文件中networkSecurityConfig文件中的system和user配置,设置不信任用户证书。
- 2.公钥证书固定。指 Client 端内置 Server 端真正的公钥证书。在 HTTPS 请求时,Server 端发给客户端的公钥证书必须与 Client 端内置的公钥证书一致,请求才会成功。
- 证书固定的一般做法是,将公钥证书(.crt 或者 .cer 等格式)内置到 App 中,然后创建 TrustManager 时将公钥证书加进去。
- 那么如何突破CA证书校验
- 第一种:JustTrustMe 破解证书固定。Xposed 和 Magisk 都有相应的模块,用来破解证书固定,实现正常抓包。破解的原理大致是,Hook 创建 SSLContext 等涉及 TrustManager 相关的方法,将固定的证书移除。
- 第二种:基于 VirtualApp 的 Hook 机制破解证书固定。在 VirtualApp 中加入 Hook 代码,然后利用 VirtualApp 打开目标应用进行抓包。具体看:[VirtualHook](https://github.com/PAGalaxyLab/VirtualHook)
#### 2.5 如何搞定加解密
- 目前使用对称加密和解密请求和响应数据
- 加密和解密都是用相同密钥。只有一把密钥,如果密钥暴露,内容就会暴露。但是这一块逆向破解有些难度。而破解解密方式就是用密钥逆向解密,或者中间人冒充使用自己的加解密方式!
- 加密后数据镇兼顾了安全性吗
- 不一定安全。中间人伪造自己的公钥和私钥,然后拦截信息,进行篡改。
#### 2.6 Charles原理
- Charles类似代理服务器
- Charles 通过将软件本身设置成系统的网络访问代理服务器,使得所有的网络请求都会走一遍 Charles 代理,从而 Charles 可以截取经过它的请求,然后我们就可以对其进行网络包的分析。
- 截取设备网络封包数据
- Charles对应设置:将代理功能打开,并设置一个固定的端口。默认情况下,端口号为:8888 。
- 移动设备设置:在手机上设置 WIFI 的 HTTP 代理。注意这里的前提是,Phone 和 Charles 代理设备链接的是同一网络(同一个ip地址和端口号)。
- 截取Https的网络封包
- 正常情况下,Charles 是不能截取Https的网络包的,这涉及到 Https 的证书问题。
#### 2.7 抓包原理图
- Charles抓包原理图
- 
- Android上的网络抓包原来是这样工作的
- [Charles抓包](https://mp.weixin.qq.com/s/kqMUbHl59V75w8xBxHbXkA)
#### 2.8 抓包核心流程
- 抓包核心流程关键节点
- 第一步,客户端向服务器发起HTTPS请求,charles截获客户端发送给服务器的HTTPS请求,charles伪装成客户端向服务器发送请求进行握手 。
- 第二步,服务器发回相应,charles获取到服务器的CA证书,用根证书(这里的根证书是CA认证中心给自己颁发的证书)公钥进行解密,验证服务器数据签名,获取到服务器CA证书公钥。然后charles伪造自己的CA证书(这里的CA证书,也是根证书,只不过是charles伪造的根证书),冒充服务器证书传递给客户端浏览器。
- 第三步,与普通过程中客户端的操作相同,客户端根据返回的数据进行证书校验、生成密码Pre_master、用charles伪造的证书公钥加密,并生成HTTPS通信用的对称密钥enc_key。
- 第四步,客户端将重要信息传递给服务器,又被charles截获。charles将截获的密文用自己伪造证书的私钥解开,获得并计算得到HTTPS通信用的对称密钥enc_key。charles将对称密钥用服务器证书公钥加密传递给服务器。
- 第五步,与普通过程中服务器端的操作相同,服务器用私钥解开后建立信任,然后再发送加密的握手消息给客户端。
- 第六步,charles截获服务器发送的密文,用对称密钥解开,再用自己伪造证书的私钥加密传给客户端。
- 第七步,客户端拿到加密信息后,用公钥解开,验证HASH。握手过程正式完成,客户端与服务器端就这样建立了”信任“。
- 在之后的正常加密通信过程中,charles如何在服务器与客户端之间充当第三者呢?
- 服务器—>客户端:charles接收到服务器发送的密文,用对称密钥解开,获得服务器发送的明文。再次加密, 发送给客户端。
- 客户端—>服务端:客户端用对称密钥加密,被charles截获后,解密获得明文。再次加密,发送给服务器端。由于charles一直拥有通信用对称密钥enc_key,所以在整个HTTPS通信过程中信息对其透明。
### 03.防止抓包思路
#### 3.1 先看如何抓包
- 使用Charles需要做哪些操作
- **1.电脑上需要安装证书**。这个主要是让Charles充当中间人,颁布自己的CA证书。
- **2.手机上需要安装证书**。这个是访问Charles获取手机证书,然后安装即可。
- **3.Android项目代码设置兼容**。Google 推出更加严格的安全机制,应用默认不信任用户证书(手机里自己安装证书),自己的app可以通过配置解决,相当于信任证书的一种操作!
- 尤其可知抓包的突破口集中以下几点
- 第一点:必须链接代理,且跟Charles要具有相同ip。**思路:客户端是否可以判断网络是否被代理了**。
- 第二点:CA证书,这一块避免使用黑科技hook证书校验代码,或者拥有修改CA证书权限。**思路:集中在可以判断是否挂载**。
- 第三点:冒充中间人CA证书,在客户端client和服务端server之间篡改拦截数据。**思路:可以做CA证书校验**。
- 第四点:为了可以在7.0上抓包,App往往配置清单文件networkSecurityConfig。**思路:线上环境去掉该配置**。
#### 3.2 设置配置文件
- 一个是CA证书配置文件
- debug包为了能够抓包,需要配置networkSecurityConfig清单文件的system和user权限,只有这样才会信任用户证书。
- 一个是检验证书配置
- 不论是权威机构颁发的证书还是自签名的,打包一份到 app 内部,比如存放在 asset 里。然后用这个KeyStore去引导生成的TrustManager来提供证书验证。
- 一个是检验域名合法性
- Android允许开发者重定义证书验证方法,使用HostnameVerifier类检查证书中的主机名与使用该证书的服务器的主机名是否一致。
- 如果重写的HostnameVerifier不对服务器的主机名进行验证,即验证失败时也继续与服务器建立通信链接,存在发生“中间人攻击”的风险。
- 如何查看CA证书的数据
- [证书验证网站](https://www.myssl.cn/tools/downloadchain.html) ;[SSL配置检查网站](https://www.geocerts.com/ssl-checker)
#### 3.3 数据加密处理
- 网络数据加密的需求
- 为了项目数据安全性,对请求体和响应体加密,那肯定要知道请求体或响应体在哪里,然后才能加密,其实都一样不论是加密url里面的query内容还是加密body体里面的都一样。
- 对数据哪里进行加密和解密
- 目前对数据返回的data进行加解密。那么如何做数据加密呢?目前项目中采用RC4加密和解密数据。
- 抓取到的内容为乱码
- 有的APP为了防止抓取,在返回的内容上做了层加密,所以从Charles上看到的内容是乱码。这种情况下也只能反编译APP,研究其加密解密算法进行解密。难度极大!
#### 3.4 避免黑科技抓包
- 基于Xposed(或者)黑科技破解证书校验
- 这种方式可以检查是否有Xposed环境,大概的思路是使用ClassLoader去加载固定包名的xp类,或者手动抛出异常然后捕获去判断是否包含Xposed环境。
- 基于VirtualApp挂载App突破证书访问权限
- 这个VirtualApp相当于是一个宿主App(可以把它想像成桌面级App),它突破证书校验。然后再实现挂载App的抓包。判断是否是双开环境!
### 04.防抓包实践开发
#### 4.1 App安全配置
- 添加配置文件
- android:networkSecurityConfig="@xml/network_security_config"
- 配置networkSecurityConfig抓包说明
- 中间人代理之所有能够获取到加密密钥就是因为我们手机上安装并信任了其代理证书,这类证书安装后都会被归结到用户证书一类,而不是系统证书。
- 那我们可以选择只信任系统内置的系统证书,而屏蔽掉用户证书(Android7.0以后就默认是只信任系统证书了),就可以防止数据被解密了。
- 实现App防抓包安全配置方式有两种:
- 一种是Android官方提供的网络安全配置;另一种也可以通过设置网络框架实现(以okhttp为例)。
- 第一种:具体可以看清单配置文件,相当于base-config标签下去掉 这组标签。
- 第二种:需要给okhttpClient配置 X509TrustManager 来监听校验服务端证书有效性。遍历设备上信任的证书,通过证书别名将用户证书(别名中含有user字段)过滤掉,只将系统证书添加到验证列表中。
- 该方案优点和缺点分析说明
- 优点:network_security_config配置简单,对整个app网络生效,无需修改代码;代码实现对通过该网络框架请求的生效,能兼容7.0以前系统。
- 缺陷:network_security_config配置方式,7.0以前的系统配置不生效,依然可以通过代理工具进行抓包。okhttp配置的方式只能对使用该网络框架进行数据传输的接口生效,并不能对整个app生效。
- 破解:将手机进行root,然后将代理证书放置到系统证书列表内,就可以绕过代码或配置检查了。
#### 4.2 关闭代理
- charles 和 fiddler 都使用代理来进行抓包,对网络客户端使用无代理模式即可防止抓包,如
``` java
OkHttpClient.Builder()
.proxy(Proxy.NO_PROXY)
.build()
```
- no_proxy实际上就是type属性为direct的一个proxy对象,这个type有三种
- direct,http,socks。这样因为是直连,所以不走代理。所以charles等工具就抓不到包了,这样一定程度上保证了数据的安全,这种方式只是通过代理抓不到包。
- 通常情况下上述的办法有用,但是无法防住使用 VPN 导流进行的抓包
- 使用VPN抓包的原理是,先将手机请求导到VPN,再对VPN的网络进行Charles的代理,绕过了对App的代理。
- 该方案优点和缺点分析说明
- 优点:实现简单方便,无系统版本兼容问题。
- 缺陷:该方案比较粗暴,将一切代理都切断了,对于有合理诉求需要使用网络代理的场景无法满足。
- 破解:使用ProxyDroid全局代理工具通过iptables对请求进行强制转发,可以有效绕过代理检测。
#### 4.3 证书校验(单向认证)
- 下载服务器端公钥证书
- 为了防止上面方案可能导致的“中间人攻击”,可以下载服务器端公钥证书,然后将公钥证书编译到Android应用中一般在assets文件夹保存,由应用在交互过程中去验证证书的合法性。
- 如何设置证书校验
- 通过OkHttp的API方法 sslSocketFactory(sslSocketFactory,trustManager) 设置SSL证书校验。
- 如何设置域名合法性校验
- 通过OkHttp的API方法 hostnameVerifier(hostnameVerifier) 设置域名合法性校验。
- 证书校验的原理分析
- 按CA证书去验证的,若不是CA可信任的证书,则无法通过验证。
- 单向认证流程图
- 
- 该方案优点和缺点分析说明
- 优点:安全性比较高,单向认证校验证书在代码中是方便的,安全性相对较高。
- 缺陷:CA证书存在过期的问题,证书升级。
- 破解:证书锁定破解比较复杂,比如老牌的JustTrustMe插件,通过hook各网络框架的证书校验方法,替换原有逻辑,使校验失效。
#### 4.4 双向认证
- 什么叫做双向认证
- SSL/TLS 协议提供了双向认证的功能,即除了 Client 需要校验 Server 的真实性,Server 也需要校验 Client 的真实性。
- 双向认证的原理
- 双向认证需要 Server 支持,Client 必须内置一套公钥证书 + 私钥。在 SSL/TLS 握手过程中,Server 端会向 Client 端请求证书,Client 端必须将内置的公钥证书发给 Server,Server 验证公钥证书的真实性。
- 用于双向认证的公钥证书和私钥代表了 Client 端身份,所以其是隐秘的,一般都是用 .p12 或者 .bks 文件 + 密钥进行存放。
- 代码层面如何做双向认证
- 双向校验就是自定义生成客户端证书,保存在服务端和客户端,当客户端发起请求时在服务端也校验客户端的证书合法性,如果不是可信任的客户端发送的请求,则拒绝响应。
- 服务端根据自身使用语言和网络框架配置相应证书校验机制即可。
- 双向认证流程图
- 
- 该方案优点和缺点分析说明
- 优点:安全性非常高,使用三方工具不易破解。
- 缺陷:服务端需要存储客户端证书,一般服务端会对应多个客户端,就需要分别存储和校验客户端证书,增加校验成本,降低响应速度。该方案比较适合对安全等级要求比较高的业务(如金融类业务)。
- 破解:由于在服务端也做校验,在服务端安全的情况下很难被攻破。
#### 4.5 防止挂载抓包
- Xposed是一个牛逼的黑科技
- Xposed + JustTrustMe 可以破解绕过校验CA证书。那么这样CA证书的校验就形同虚设了,对App的危险性也很大。
- App多开运行在多个环境上
- 多开App的原理类似,都是以新进程运行被多开的App,并hook各类系统函数,使被多开的App认为自己是一个正常的App在运行。
- 一种是从多开App中直接加载被多开的App,如平行空间、VirtualApp等,另一种是让用户新安装一个App,但这个App本质上就是一个壳,用来加载被多开的App。
- VirtualApp是一个牛逼的黑科技
- 它破坏了Android 系统本身的隔离措施,可以进行免root hook和其他黑科技操作,你可以用这个做很多在原来APP里做不到事情,于此同时Virtual App的安全威胁也不言而喻。
- 如何判断是否具有Xposed环境
- 第一种方式:获取当前设备所有运行的APP,根据安装包名对应用进行检测判断是否有Xposed环境。
- 第二种方式:通过自造异常来检测堆栈信息,判断异常堆栈中是否包含Xposed等字符串。
- 第三种方式:通过ClassLoader检查是否已经加载了XposedBridge类和XposedHelpers类来检测。
- 第四种方式:获取DEX加载列表,判断其中是否包含XposedBridge.jar等字符串。
- 第五种方式:检测Xposed相关文件,通过读取/proc/self/maps文件,查找Xposed相关jar或者so文件来检测。
- 如何判断是否是双开环境
- 第一种方式:通过检测app私有目录,多开后的应用路径会包含多开软件的包名。还有一种思路遍历应用列表如果出现同样的包名,则被认为双开了。
- 第二种方式:如果同一uid下有两个进程对应的包名,在"/data/data"下有两个私有目录,则该应用被多开了。
- 判断了具有xposed或者多开环境怎么处理App
- 目前使用VirtualApp挂载,或者Xposed黑科技去hook,前期可以先用埋点统计。测试学而思App发现挂载在VA上是推出App。
#### 4.5 数据加解密
- 针对数据加解密入口
- 目前在网络请求类里添加拦截器,然后在拦截器中处理request请求和response响应数据的加密和解密操作。
- 主要是加密什么数据
- 在request请求数据阶段,如果是get请求加密url数据,如果是post请求则加密url数据和requestBody数据。
- 在response响应数据阶段,
- 如何进行加密:发起请求(加密)
- 第一步:获取请求的数据。主要是获取请求url和requestBody,这一块需要对数据一块处理。
- 第二步:对请求数据进行加密。采用RC4加密数据
- 第三步:根据不同的请求方式构造新的request。使用 key 和 result 生成新的 RequestBody 发起网络请求
- 如何进行解密:接收返回(解密)
- 第一步:常规解析得到 result ,然后使用RC4工具,传入key去解密数据得到解密后的字符串
- 第二步:将解密的字符串组装成ResponseBody数据传入到body对象中
- 第三步:利用response对象去构造新的response,然后最后返回给App
#### 4.7 证书锁定
- 证书锁定是Google官方比较推荐的一种校验方式
- 原理是在客户端中预先设置好证书信息,握手时与服务端返回的证书进行比较,以确保证书的真实性和有效性。
- 如何实现证书锁定
- 有两种实现方式:一种通过network_security_config.xml配置,另一种通过代码设置;
``` java
//第一种方式:配置文件
api.zuoyebang.cn
38JpactkIAq2Y49orFOOQKurWxmmSFZhBCoQYcRhK90=
9k1a0LRMXouZHRC8Ei+4PyuldPDcf3UKgO/04cDM90K=
//第二种方式:代码设置
fun sslPinning(): OkHttpClient {
val builder = OkHttpClient.Builder()
val pinners = CertificatePinner.Builder()
.add("api.zuoyebang.cn", "sha256//89KpactkIAq2Y49orFOOQKurWxmmSFZhBCoQYcRh00L=")
.add("api.zuoyebang.com", "sha256//a8za0LRMXouZHRC8Ei+4PyuldPDcf3UKgO/04cDM1o=09")
.build()
builder.apply {
certificatePinner(pinners)
}
return builder.build()
}
```
- 该方案优点和缺点分析说明
- 优点:安全性高,配置方式也比较简单,并能实现动态更新配置。
- 缺陷:网络安全配置无法实现证书证书的动态更新,另外该配置也受Android系统影响,对7.0以前的系统不支持。代码配置相对灵活些。
- 破解:证书锁定破解比较复杂,比如老牌的JustTrustMe插件,通过hook各网络框架的证书校验方法,替换原有逻辑,使校验失效
#### 4.8 Sign签名
- 先说一下背景和问题
- http://api.test.com/getbanner?key1=value1&key2=value2&key3=value3
- 这种方式简单粗暴,通过调用getbanner方法即可获取轮播图列表信息,但是这样的方式会存在很严重的安全性问题,没有进行任何的验证,大家都可以通过这个方法获取到数据,导致产品信息泄露。
- 在写开放的API接口时是如何保证数据的安全性的?
- 请求来源(身份)是否合法?请求参数被篡改?请求的唯一性(不可复制)?
- 问题的解决方案设想
- 解决方案:为了保证数据在通信时的安全性,我们可以采用参数签名的方式来进行相关验证。
- 最终决定的解决方案
- 调用接口之前需要验证签名和有效时间,要生成一个sign签名。先拼接-后转码-再加密-再发请求!
- sign签名校验实践
- 需要对请求参数进行签名验证,签名方式如下:key1=value1&key2=value2&key3=value3&secret=yc 。对这个字符串进行md5一下。
- 然后被sign后的接口就变成了:http://api.test.com/getbanner?key1=value1&key2=value2&key3=value3&sign=xxx
- 为什么在获取sign的时候建议使用secret参数?secret仅作加密使用,添加在参数中主要是md5,为了保证数据安全请不要在请求参数中使用。
- 服务端对sign校验
- 这样请求的时候就需要合法正确签名sign才可以获取数据。这样就解决了身份验证和防止参数篡改问题,如果请求参数被人拿走,没事,他们永远也拿不到secret,因为secret是不传递的。再也无法伪造合法的请求。
- 如何保证请求的唯一性
- http://api.test.com/getbanner?key1=value1&key2=value2&key3=value3&sign=xxx&stamp=201803261407
- 通过stamp时间戳用来验证请求是否过期。这样就算被人拿走完整的请求链接也是无效的。
- Sign签名安全性分析:
- 通过上面的案例,安全的关键在于参与签名的secret,整个过程中secret是不参与通信的,所以只要保证secret不泄露,请求就不会被伪造。
### 05.架构设计说明
#### 5.1 整体架构设计
#### 5.2 关键流程图
#### 5.3 稳定性设计
- 对于请求和响应的数据加解密要注意
- 在网络上交换数据(网络请求数据)时,可能会遇到不可见字符,不同的设备对字符的处理方式有一些不同。
- Base64对数据内容进行编码来适合传输。准确说是把一些二进制数转成普通字符用于网络传输。统统变成可见字符,这样出错的可能性就大降低了。
#### 5.4 降级设计
- 可以一键配置AB测试开关
```
.setMonitorToggle(object : IMonitorToggle {
override fun isOpen(): Boolean {
//todo 是否降级,如果降级,则不使用该功能。留给AB测试开关
return false
}
})
```
#### 5.5 异常设计说明
- base64加密和解密导致错误问题
- Android 有自带的Base64实现,flag要选Base64.NO_WRAP,不然末尾会有换行影响服务端解码。导致解码失败。
#### 5.6 Api文档
- 关于初始化配置
``` java
NotCaptureHelper.getInstance().config = CaptureConfig.builder()
//设置debug模式
.setDebug(true)
//设置是否禁用代理
.setProxy(false)
//设置是否进行数据加密和解密,
.setEncrypt(true)
//设置cer证书路径
.setCerPath("")
//设置是否进行CA证书校验
.setCaVerify(false)
//设置加密和解密key
.setEncryptKey(key)
//设置参数
.setReservedQueryParam(OkHttpBuilder.RESERVED_QUERY_PARAM_NAMES)
.setMonitorToggle(object : IMonitorToggle {
override fun isOpen(): Boolean {
//todo 是否降级,如果降级,则不使用该功能。留给AB测试开关
return false
}
})
.build()
```
- 设置okHttp配置
``` java
NotCaptureHelper.getInstance().setOkHttp(app,okHttpBuilder)
```
- 如何设置自己的加解密方式
``` java
NotCaptureHelper.getInstance().encryptDecryptListener = object : EncryptDecryptListener {
/**
* 外部实现自定义加密数据
*/
override fun encryptData(key: String, data: String): String {
LoggerReporter.report("NotCaptureHelper", "encryptData data : $data")
val str = data.encryptWithRC4(key) ?: ""
LoggerReporter.report("NotCaptureHelper", "encryptData str : $str")
return str
}
/**
* 外部实现自定义解密数据
*/
override fun decryptData(key: String, data: String): String {
LoggerReporter.report("NotCaptureHelper", "decryptData data : $data")
val str = data.decryptWithRC4(key) ?: ""
LoggerReporter.report("NotCaptureHelper", "decryptData str : $str")
return str
}
}
```
#### 5.7 防抓包功能自测
- 网络请求测试
- 正常请求,测试网络功能是否正常
- 抓包测试
- 配置fiddler,charles等工具
- 手机上设置代理
- 手机上安装证书
- 单向认证测试:进行网络请求,会提示SSLHandshakeException即ssl握手失败的错误提示,即表示app端的单向认证成功。
- 数据加解密:进行网络请求,看一下请求参数和响应body数据是否加密,如果看不到实际json实体则表示加密成功。
================================================
FILE: app/.gitignore
================================================
/build
================================================
FILE: app/build.gradle
================================================
apply plugin: 'com.android.application'
apply from: rootProject.projectDir.absolutePath + "/yc.gradle"
android {
compileSdkVersion rootProject.ext.android["compileSdkVersion"]
//buildToolsVersion rootProject.ext.android["buildToolsVersion"]
defaultConfig {
applicationId "com.ycbjie.ycreddotview"
minSdkVersion rootProject.ext.android["minSdkVersion"]
targetSdkVersion rootProject.ext.android["targetSdkVersion"]
versionCode rootProject.ext.android["versionCode"]
versionName rootProject.ext.android["versionName"]
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation rootProject.ext.dependencies["appcompat"]
implementation rootProject.ext.dependencies["annotation"]
implementation(rootProject.ext.dependencies["okhttp"])
implementation(rootProject.ext.dependencies["gson"])
implementation project(path: ':NotCaptureLib')
//同上上报库
implementation 'com.github.yangchong211.YCCommonLib:EventUploadLib:1.4.3'
//通用组件接口库
implementation 'com.github.yangchong211.YCCommonLib:AppCommonInter:1.4.3'
//加解密库
//implementation project(path: ':AppEncryptLib')
implementation 'com.github.yangchong211.YCCommonLib:AppEncryptLib:1.4.3'
}
================================================
FILE: app/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
================================================
FILE: app/src/main/AndroidManifest.xml
================================================
================================================
FILE: app/src/main/java/com/yc/toolapp/App.java
================================================
package com.yc.toolapp;
import android.app.Application;
public class App extends Application {
@Override
public void onCreate() {
super.onCreate();
}
}
================================================
FILE: app/src/main/java/com/yc/toolapp/MainActivity.kt
================================================
package com.yc.toolapp
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.yc.appcommoninter.IMonitorToggle
import com.yc.appencryptlib.AesEncryptUtils
import com.yc.eventuploadlib.LoggerReporter
import com.yc.notcapturelib.helper.CaptureConfig
import com.yc.notcapturelib.helper.EncryptDecryptListener
import com.yc.notcapturelib.helper.NotCaptureHelper
import okhttp3.OkHttpClient
import java.util.concurrent.TimeUnit
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
private fun init(){
NotCaptureHelper.getInstance().config = CaptureConfig.builder()
//设置debug模式
.setDebug(true)
//设置是否禁用代理
.setProxy(false)
//设置是否进行数据加密和解密,
.setEncrypt(true)
//设置cer证书路径
.setCerPath("")
//设置是否进行CA证书校验
.setCaVerify(false)
//设置加密和解密key
.setEncryptKey("key")
.setMonitorToggle(object : IMonitorToggle {
override fun isOpen(): Boolean {
//todo 是否降级,如果降级,则不使用该功能。留给AB测试开关
return false
}
})
.build()
NotCaptureHelper.getInstance().encryptDecryptListener = object : EncryptDecryptListener {
/**
* 外部实现自定义加密数据
*/
override fun encryptData(key: String, data: String): String {
LoggerReporter.report("NotCaptureHelper", "encryptData data : $data")
val str = AesEncryptUtils.encrypt(data, key)
LoggerReporter.report("NotCaptureHelper", "encryptData str : $str")
return str
}
/**
* 外部实现自定义解密数据
*/
override fun decryptData(key: String, data: String): String {
LoggerReporter.report("NotCaptureHelper", "decryptData data : $data")
val str = AesEncryptUtils.decrypt(data, key)
LoggerReporter.report("NotCaptureHelper", "decryptData str : $str")
return str
}
}
}
private fun setOkHttp() {
val builder = OkHttpClient.Builder()
val okHttpClient = builder
.connectTimeout(15, TimeUnit.SECONDS)
.writeTimeout(20, TimeUnit.SECONDS)
.readTimeout(20, TimeUnit.SECONDS)
.build()
NotCaptureHelper.getInstance().setOkHttp(this,builder)
}
}
================================================
FILE: app/src/main/res/drawable/ic_launcher_background.xml
================================================
================================================
FILE: app/src/main/res/drawable-v24/ic_launcher_foreground.xml
================================================
================================================
FILE: app/src/main/res/layout/activity_main.xml
================================================
================================================
FILE: app/src/main/res/layout/component_banner.xml
================================================
================================================
FILE: app/src/main/res/layout/fragment.xml
================================================
================================================
FILE: app/src/main/res/layout/main_tab_layout.xml
================================================
================================================
FILE: app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
================================================
================================================
FILE: app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
================================================
================================================
FILE: app/src/main/res/values/colors.xml
================================================
#3F51B5
#303F9F
#FF4081
#ffe935
================================================
FILE: app/src/main/res/values/strings.xml
================================================
YCRedDotView
================================================
FILE: app/src/main/res/values/styles.xml
================================================
================================================
FILE: build.gradle
================================================
apply from: "yc.gradle"
buildscript {
apply from: 'yc.gradle'
repositories {
google()
jcenter()
maven {
url 'https://maven.google.com/'
name 'Google'
}
//添加阿里云镜像
maven { url "https://maven.aliyun.com/repository/public" }
maven { url "https://maven.aliyun.com/repository/google" }
maven { url "https://maven.aliyun.com/repository/jcenter" }
maven { url 'https://jitpack.io' }
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.1.0'
//classpath 'com.android.tools.build:gradle:3.2.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
//jitpack
classpath 'com.github.dcendents:android-maven-gradle-plugin:2.1'
//jetpack
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.0.0"
//自定义插件
//classpath 'com.github.yangchong211.YCAppTool:ServiceLoaderPlugin:1.4.3'
//classpath 'com.github.yangchong211.YCAppTool:BuildTimeCostPlugin:1.4.3'
}
}
allprojects {
repositories {
google()
jcenter()
maven {
url 'https://maven.google.com/'
name 'Google'
}
maven { url 'https://jitpack.io' }
//添加阿里云镜像
maven { url "https://maven.aliyun.com/repository/public" }
maven { url "https://maven.aliyun.com/repository/google" }
maven { url "https://maven.aliyun.com/repository/jcenter" }
mavenCentral()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
================================================
FILE: gradle/wrapper/gradle-wrapper.properties
================================================
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
================================================
FILE: gradle.properties
================================================
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx1536m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
android.enableAapt2=false
android.enableJetifier=true
android.useAndroidX=true
================================================
FILE: gradlew
================================================
#!/usr/bin/env sh
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=$(save "$@")
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
exec "$JAVACMD" "$@"
================================================
FILE: gradlew.bat
================================================
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
================================================
FILE: settings.gradle
================================================
include ':app'
include ':NotCaptureLib'
================================================
FILE: yc.gradle
================================================
ext {
isApplication = false //false:作为Lib组件存在, true:作为application存在,这个不建议改
isJetpackApplication = true
isOtherApplication = false //其他模块开关,false:作为Lib组件存在, true:作为application存在
isAnimApplication = false
android = [
compileSdkVersion: 29,
buildToolsVersion: "29.0.0",
minSdkVersion : 17,
targetSdkVersion : 29,
versionCode : 22,
versionName : "1.8.2" //必须是int或者float,否则影响线上升级
]
//AndroidX系列
appcompatVersion = '1.2.0'
annotationVersion = '1.2.0'
cardviewVersion = '1.0.0'
mediaVersion = '1.0.1'
swiperefreshlayoutVersion = '1.0.0'
materialVersion = '1.0.0-rc01'
coordinatorlayoutVersion = '1.0.0'
constraintlayoutVersion = '1.1.3'
recyclerviewVersion = '1.0.0'
multidexVersion = '1.0.2'
viewpagerVersion = '1.0.0'
//AndroidX系列ktx
activityKtx = '1.4.0'
//kotlin系列
kotlin_version = '1.4.31'
kotlinxVersion = '1.0.1'
//jetpack系列
coreVersion = '1.0.0'
databindingVersion = '3.2.1'
archLifecycleVersion = '2.2.0'
roomVersion = '2.0.0'
workVersion = '2.0.0'
//第三方库系列
retrofitSdkVersion = "2.4.0"
glideSdkVersion = "4.9.0"
okhttpVersion = "4.7.2"
gsonVersion = "2.8.5"
permissionsVersion = "1.0.1"
dependencies = [
//AndroidX系列
appcompat : "androidx.appcompat:appcompat:${appcompatVersion}",
annotation : "androidx.annotation:annotation:${annotationVersion}",
constraintlayout : "androidx.constraintlayout:constraintlayout:${constraintlayoutVersion}",
coordinatorlayout : "androidx.coordinatorlayout:coordinatorlayout:${coordinatorlayoutVersion}",
cardview : "androidx.cardview:cardview:${cardviewVersion}",
recyclerview : "androidx.recyclerview:recyclerview:${recyclerviewVersion}",
media : "androidx.media:media:${mediaVersion}",
material : "com.google.android.material:material:${materialVersion}",
swiperefreshlayout : "androidx.swiperefreshlayout:swiperefreshlayout:${swiperefreshlayoutVersion}",
"multidex" : "com.android.support:multidex:$multidexVersion",
viewpager2 : "androidx.viewpager2:viewpager2:${viewpagerVersion}",
//jetpack系列
core : "androidx.core:core:${coreVersion}",
coreKtx : "androidx.core:core-ktx:${coreVersion}",
roomRuntime : "androidx.room:room-runtime:${roomVersion}",
roomCompiler : "androidx.room:room-compiler:${roomVersion}",
databinding : "androidx.databinding:databinding-common:${databindingVersion}",
lifecycle : "androidx.lifecycle:lifecycle-extensions:${archLifecycleVersion}",
lifecycleCompiler : "androidx.lifecycle:lifecycle-common:${archLifecycleVersion}",
lifecycleRuntime : "androidx.lifecycle:lifecycle-runtime:${archLifecycleVersion}",
livedataCore : "androidx.lifecycle:lifecycle-livedata-core:${archLifecycleVersion}",
workKtx : "androidx.work:work-runtime-ktx:${workVersion}",
activityKtx : "androidx.activity:activity-ktx:${activityKtx}",
navigationFragment : "androidx.navigation:navigation-fragment:${archLifecycleVersion}",
navigationFragmentKtx : "androidx.navigation:navigation-fragment-ktx:${archLifecycleVersion}",
navigationUiKtx : "androidx.navigation:navigation-ui-ktx:${archLifecycleVersion}",
activityKtx : "androidx.activity:activity-ktx:${activityKtx}",
datastore : "androidx.datastore:datastore-preferences:${coreVersion}",
//将 Kotlin 协程与生命周期感知型组件一起使用
//https://developer.android.com/topic/libraries/architecture/coroutines
lifecycleKtx : "androidx.lifecycle:lifecycle-runtime-ktx:${archLifecycleVersion}",
livedataKtx : "androidx.lifecycle:lifecycle-livedata-ktx:${archLifecycleVersion}",
viewmodelKtx : "androidx.lifecycle:lifecycle-viewmodel-ktx:${archLifecycleVersion}",
//kotlin
kotlinxCoroutinesCore : "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinxVersion",
kotlinxCoroutinesAndroid : "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlinxVersion",
kotlinxJdk : "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version",
//network
retrofit : "com.squareup.retrofit2:retrofit:$retrofitSdkVersion",
"retrofit-converter-gson" : "com.squareup.retrofit2:converter-gson:$retrofitSdkVersion",
"retrofit-adapter-rxjava2": "com.squareup.retrofit2:adapter-rxjava2:$retrofitSdkVersion",
okhttp : "com.squareup.okhttp3:okhttp:$okhttpVersion",
gson : "com.google.code.gson:gson:$gsonVersion",
glide : "com.github.bumptech.glide:glide:$glideSdkVersion",
"glide-compiler" : "com.github.bumptech.glide:compiler:$glideSdkVersion",
//widget库
//https://github.com/yangchong211/YCWidgetLib
"ShadowConfig" : "com.github.yangchong211.YCWidgetLib:ShadowConfig:1.0.5",
"RedDotView" : "com.github.yangchong211.YCWidgetLib:RedDotView:1.0.5",
"ExpandPager" : "com.github.yangchong211.YCWidgetLib:ExpandPager:1.0.5",
"ExpandLib" : "com.github.yangchong211.YCWidgetLib:ExpandLib:1.0.5",
"CardViewLib" : "com.github.yangchong211.YCWidgetLib:CardViewLib:1.0.5",
"RoundCorners" : "com.github.yangchong211.YCWidgetLib:RoundCorners:1.0.5",
//线程池
//https://github.com/yangchong211/YCThreadPool
"ThreadPoolLib" : "com.github.yangchong211.YCThreadPool:ThreadPoolLib:1.3.7",
"ThreadTaskLib" : "com.github.yangchong211.YCThreadPool:ThreadTaskLib:1.3.7",
"EasyExecutor" : "com.github.yangchong211.YCThreadPool:EasyExecutor:1.3.8",
//效率优化
//https://github.com/yangchong211/YCEfficient
"AppStartLib" : "com.github.yangchong211.YCEfficient:AppStartLib:1.3.1",
"AppProcessLib" : "com.github.yangchong211.YCEfficient:AppProcessLib:1.3.1",
"AutoCloserLib" : "com.github.yangchong211.YCEfficient:AutoCloserLib:1.3.1",
//tools
"easypermissions" : "pub.devrel:easypermissions:$permissionsVersion",
]
}