通常,我们讨论的话题都是如何写出强大的功能。
但今天,咱们换个视角,聊点不一样的------
应用安全:防防防,反反反。防,不是防用户,而是防"逆向视角下的你自己"。
当攻击者拿到你的 APK / IPA / 可执行文件时,
TA 不关心你的业务多优雅、架构多清晰,
TA 只关心三件事:
- 能不能看懂
- 能不能改
- 能不能利用,搞钱
所谓"防",本质上就是:
让逆向工程变得更慢、更贵、更不确定。
防静态分析、防动态注入、防抓包、防内存Dump、防SO替换、防Inline Hook、防PLT/GOT劫持、防Java Hook、防Native Hook、防Xposed、防Substrate、防Frida、防Magisk模块、防VirtualXposed、防LSPosed、反调试、反单步、反附加、反Trace、反调试器伪装、反ROOT、 反解锁、反系统篡改、反环境伪造、防重打包、防二次签名、防资源替换、防DEX注入、防脚本、 防自动化、防模拟器、防云手机、防小三,没有啦,后面这个是我刻意搞怪😏加上去的。
RSA + AES 加密
防逆向工程的第一步,往往不是混淆、不是反调试,而是加密 。
因为只要数据是明文,所有防护最终都会变成"延迟被看懂的时间"。
在密码学的江湖里,最常被拎出来的两位老前辈,非 RSA 非对称加密 和 AES 对称加密 莫属。
一个安全、稳重、名门正派,但出手慢;
一个手快、效率高、杀伤力强,但最大的软肋就是------密钥怎么安全地交出去。
所以现实世界里,从来不是二选一。
小孩子才做选择,而你,全都要。
你不慌不忙,先在客户端本地生成了一把随机的 AES Key 。
这把 key 很短命,只服务当前这一次通信,用完就丢,连自己都不打算再认。
你用它把真正的业务数据------资源、配置、秘密、惊喜------统统加密成一坨谁也看不懂的密文。
但你心里很清楚:
AES 再快,也怕 key 泄露。
于是你转身请出了 RSA。
你拿起对象早就公开在客户端里的 RSA 公钥 ,
把刚才那把 AES Key 加密一层。
现在好了:
- 数据,被 AES 锁住
- AES 的钥匙,又被 RSA 锁住
双保险。
你把这两样东西------
RSA 加密过的 AES Key 和 AES 加密后的密文
一并发了出去。
此时,无论是抓包、代理、小三、还是半路劫持的人,
看到的都只是两坨"看不懂,也用不了"的东西。
另一头,你女朋友收到了这份"神秘快递"。
她当然知道怎么玩这套。
她先拿出自己珍藏、从不外泄的 RSA 私钥 ,
轻轻一解,
RSA 的包装就被拆开,真正的 AES Key 映入眼帘。
然后,她用这把 key,
把那段密文慢慢解开。
数据复原,信息重现,惊喜如约而至。
kotlin
package site.doramusic.app.http
import dora.util.CryptoUtils
import java.security.SecureRandom
object SecureRequestBuilder {
const val AES_KEY_LENGTH = 16 // bytes (128位)
const val RSA_PUBLIC = "" // 等待公钥...
enum class SecureMode {
NONE,
ENC,
ENC_SIGN
}
/**
* 获取随机key。
*/
@JvmStatic
fun getRandomKey(): String {
val sb = StringBuilder(AES_KEY_LENGTH)
val random = SecureRandom()
repeat(AES_KEY_LENGTH) {
when (random.nextInt(3)) {
0 -> {
// 0-9
sb.append(random.nextInt(10))
}
1 -> {
// A-Z
sb.append((random.nextInt(26) + 'A'.code).toChar())
}
2 -> {
// a-z
sb.append((random.nextInt(26) + 'a'.code).toChar())
}
}
}
return sb.toString()
}
@JvmStatic
fun build(
req: BaseReq,
mode: SecureMode
): ReqBody? {
return when (mode) {
// 明文
SecureMode.NONE -> {
ReqBody(
mode = "NONE",
data = req.payload
)
}
// 端到端加密
SecureMode.ENC -> {
val aesKey = getRandomKey()
ReqBody(
mode = "ENC",
key = CryptoUtils.encryptByPublic(RSA_PUBLIC, aesKey),
data = CryptoUtils.encryptAES(aesKey, req.payload)
)
}
// 端到端加密 + 客户端签名
SecureMode.ENC_SIGN -> {
// 不告诉你,这个项目不提供可信客户端能力
null
}
}
}
}
使用SecureRandom生成完全随机的key。
kotlin
package site.doramusic.app.http
import com.google.gson.Gson
import dora.util.GlobalContext
import dora.util.LanguageUtils
import java.lang.reflect.Modifier
import java.util.Locale
abstract class BaseReq {
/**
* 根据不同的语种返回本地化的内容。
*/
var lang: String = ""
/**
* 数据载体。
*/
var payload: String = ""
/**
* 防抓包伪造签名重复请求,签名过期,拒绝请求。
*/
var timestamp: String = ""
/**
* 可信客户端签名,ENC_SIGN模式下,签名不正确,拒绝请求。
*/
var signature: String? = null
init {
lang = LanguageUtils.getLangTag(GlobalContext.get()).ifEmpty { Locale.getDefault().language }
timestamp = (System.currentTimeMillis() / 1000).toString()
}
/**
* 对数据进行排序,保证唯一性,返回排序后的JSON字符串。
*/
fun sort(): String {
val map = sortedMapOf<String, Any?>()
var clazz: Class<*>? = this.javaClass
while (clazz != null && clazz != BaseReq::class.java) {
clazz.declaredFields
.filter { field ->
!field.isSynthetic &&
!Modifier.isStatic(field.modifiers)
}
.forEach { field ->
field.isAccessible = true
map[field.name] = field[this]
}
clazz = clazz.superclass
}
// 父类字段(显式加入,避免遗漏)
map["lang"] = lang
map["payload"] = payload
map["timestamp"] = timestamp
return Gson().toJson(map)
}
}
大致思路如上,如需查看完整实现与工程细节,可直接进入这个传送门:
github.com/dora4/DoraM...。
复用
能提供无限自由组合的复用能力 ,是大工程得以长期演进的基石。
复用不是复制粘贴,也不是简单封装几个工具类,而是结构层面的能力释放。
抽象与提炼的水平,决定了一套工程的成熟度。
你抽的是"功能",还是"能力";
你复用的是"代码",还是"模型";
这些选择,会在项目规模放大之后,给出完全不同的回报。
当架构设计足够清晰、边界足够稳定时,
你甚至可以做到------
一份后端代码,对接 n 个承载同一品牌理念的 App。
你只需要把真正"通用"的东西抽出来:
- 建议与反馈
- FAQ / 帮助中心
- App 版本分发与灰度控制
- 首页横幅广告
- 首页直播间 / 内容推荐
- 配置信息与功能开关
- 系统通知
- 促销活动
- 埋点统计与用户行为分析
这些模块,只写一次,
却可以被全品牌、全产品线、全形态的 App 反复使用。
而客户端,只负责表现、体验和差异化。
真正复杂、真正需要稳定演进的部分,
被牢牢收敛在可控的架构之中。
最终你会发现:
安全只是起点,
加密只是手段,
架构与复用,才是工程走得远的根本原因。
工程不是写完一次就结束,
而是要经得起规模、时间和变化的反复考验。