做了个注册机给桌面软件签发许可证,注册机本身有个管理员解锁密码——不是最终用户的密码,是防止谁都能进来乱签 license 的那道门。一开始图省事,密码明文写在 gradle.properties 里,程序运行的时候读出来比对。能用是能用,但越想越不对劲。

问题在于,这个密码随包一起发出去了。gradle.properties 里的值,打包完之后会以某种形式落到产物里,最差就是直接生成一个 register.properties 跟着 exe 走。破解者拿到发版包,翻一翻配置文件,明文密码就在那儿躺着,这道门等于没上锁。

后来改成这样:密码在打包阶段就转成 MD5,然后通过 exe4j 的 VM 参数固化进 exe,运行时直接从系统属性里读 MD5 来比对,全程不落外部配置文件。

打包脚本里大致是:

// 打包时把明文密码算成 MD5,塞进 exe4j 的 VM 参数
def pwdMd5 = md5(project.property('registerPassword'))
def vmArgs = "-Dregister.password.md5=${pwdMd5}"
// 这段 vmArgs 写进 exe4j 配置,随 exe 一起固化

运行时校验这边,按优先级找这个 MD5:

private String resolveExpectedPasswordMd5() {
// 1. 打包时通过 -Dregister.password.md5 注入的(生产)
String md5 = System.getProperty("register.password.md5");
if (StringUtils.isNotEmpty(md5)) {
return md5;
}
// 2. 开发态 IDEA 直接跑,读 properties 里的明文现算(仅本机)
String pwd = loadDevProperties().getProperty("registerPassword");
if (StringUtils.isNotEmpty(pwd)) {
return md5(pwd);
}
// 3. 兜底
return md5(FALLBACK_PASSWORD);
}

用户输入密码,同样 md5 一遍,跟期望值比,对得上才放行。全程只存和比 MD5,明文密码不进运行态、不落盘。

这么改之后几个好处。一是发版包里翻不到明文密码了,最起码「翻配置文件」这条路堵死了,想拿原值得反 exe。二是 VM 参数 -D 是打进 exe4j 配置、随 exe 固化的,不是旁边带个文件,没那么容易被一眼发现和替换。三是开发态不受影响——IDEA 里直接跑 main 的时候没有那个 VM 参数,就走第二条读 properties 现算,本机调试照样方便。

当然这不是什么高强度的防护,MD5 本身早不该直接用来存密码(正经得加盐、用慢哈希),注册机这个场景门槛要求没那么高,这么搞够用。真要紧的是别把明文密码跟包一起发出去——那不叫加密,那叫附赠钥匙。