build(android): release 改用正式签名,并禁止静默降级
改的是什么:client/android/app/build.gradle.kts 此前是 Flutter 模板原样 ——
release 一直用 signingConfigs.getByName("debug"),即公开的 AOSP 测试密钥。
三条后果且都不报错:
① 任何人都能签一个同签名的包冒充升级
② 无法上架(Play 拒绝 debug 签名)
③ 换正式密钥后,已装设备无法覆盖升级(不可逆)
怎么改:
- 新增 signingConfigs.release,凭据从 client/android/key.properties 读
(该文件已在 .gitignore 中,确认 check-ignore 命中)
- buildTypes.release 指向它
- task-graph 守卫:release 构建缺 key.properties 时**明确失败**并打印修复指引
刻意不学模板「回落 debug 签名」—— 那是一条「看着构建成功、其实签的是公开密钥」
的静默失败路径。本仓库多次栽在静默失败上(迁移从不执行、首次部署路径无人测),
故这里宁可让它红。
验证:
- 变异:移走 key.properties → release 构建 2 秒内失败并给出指引
- 放回 → 构建成功;apksigner 确认 DN=CN=InkWatch,指纹与 keystore 一致
SHA-256 0d58f8b5749b6a60c8042f6f4333a0c847192bf37d44e56cdd4756f3970ad679
密钥:~/keystores/inkwatch-release.jks(仓库外,PKCS12/RSA2048,有效期至 2054),
凭据记录在同目录一个 600 权限的文件里。
⚠️ 这是本仓库唯一不可再生的东西 —— 丢了就永远无法更新已发布的应用。
新增 docs/release-and-signing.md:密钥信息、key.properties 契约、出包命令、
两个包名的分工、换签名的后果与恢复步骤。
This commit is contained in:
1 parent
158ae7577e
commit
0777e55cf5
2 files changed
+170
-4
No files matched your search
@@ -1,9 +1,28 @@
|
||||
import java.io.FileInputStream
|
||||
import java.util.Properties
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
||||
id("dev.flutter.flutter-gradle-plugin")
|
||||
}
|
||||
|
||||
// ── release 签名:正式密钥,且**禁止静默降级** ──────────────────────
|
||||
// 口令等敏感值放在 `client/android/key.properties`(已被 .gitignore 排除)。
|
||||
//
|
||||
// 背景(2026-09-16 修的):本文件此前一直是 Flutter 模板的原样 ——
|
||||
// buildTypes.release { signingConfig = signingConfigs.getByName("debug") }
|
||||
// 那等于用**公开的 AOSP 测试密钥**签名:谁都能签一个同签名的包冒充升级,
|
||||
// 而且无法上架。更糟的是它**不报错**,构建看起来一切正常。
|
||||
// 故这里改成:走正式密钥;缺配置时 release 构建**明确失败**(见文件末尾的守卫),
|
||||
// 而不是像模板那样回落 debug —— 静默失败比失败本身更贵。
|
||||
val keystorePropsFile = rootProject.file("key.properties")
|
||||
val keystoreProps = Properties().apply {
|
||||
if (keystorePropsFile.exists()) {
|
||||
FileInputStream(keystorePropsFile).use { load(it) }
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.pmon.pmon_client"
|
||||
compileSdk = flutter.compileSdkVersion
|
||||
@@ -14,8 +33,21 @@ android {
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
create("release") {
|
||||
if (keystorePropsFile.exists()) {
|
||||
storeFile = file(keystoreProps.getProperty("storeFile"))
|
||||
storePassword = keystoreProps.getProperty("storePassword")
|
||||
keyAlias = keystoreProps.getProperty("keyAlias")
|
||||
keyPassword = keystoreProps.getProperty("keyPassword")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
// ⚠️ 这是**主干(通用版)**的包名,已装机不可更改语义,见 docs/rename-inkwatch.md。
|
||||
// 水墨屏专属版(feature/eink 分支)刻意用**另一个** applicationId,
|
||||
// 否则两个版本在同一台手机上会互相覆盖(且 versionCode 小的会被当成降级)。
|
||||
applicationId = "com.pmon.pmon_client"
|
||||
// You can update the following values to match your application needs.
|
||||
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||
@@ -31,13 +63,38 @@ android {
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
// TODO: Add your own signing config for the release build.
|
||||
// Signing with the debug keys for now, so `flutter run --release` works.
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
signingConfig = signingConfigs.getByName("release")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// release 构建缺 key.properties 时**明确失败**。
|
||||
//
|
||||
// 为什么不改回"回落到 debug 签名":Flutter 模板默认那么干,结果是
|
||||
// "看着构建成功、其实签的是公开测试密钥" —— 一条谁也发现不了的静默失败路径。
|
||||
// 这里宁可让它红。
|
||||
gradle.taskGraph.whenReady {
|
||||
val needsSigning = allTasks.any {
|
||||
it.name == "assembleRelease" || it.name == "bundleRelease"
|
||||
}
|
||||
if (needsSigning && !keystorePropsFile.exists()) {
|
||||
throw GradleException(
|
||||
"""
|
||||
|release 构建需要签名配置,但 ${keystorePropsFile.path} 不存在。
|
||||
|
|
||||
|请创建该文件(已被 .gitignore 排除,**不要提交**):
|
||||
| storeFile=/绝对路径/inkwatch-release.jks
|
||||
| storePassword=...
|
||||
| keyAlias=...
|
||||
| keyPassword=...
|
||||
|
|
||||
|注意:不要为了"先跑起来"而回落到 debug 签名 —— 那是公开密钥,
|
||||
|任何人都能签一个同签名的包冒充你的应用升级。
|
||||
""".trimMargin()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
# 发布与签名(墨观 InkWatch · Android)
|
||||
|
||||
> 2026-09-16 立。此前 release 一直用 Flutter 模板的默认行为 —— **debug 签名**。
|
||||
|
||||
## ⚠️ 为什么签名这件事必须先说
|
||||
|
||||
`client/android/app/build.gradle.kts` 里原本是模板原样:
|
||||
|
||||
```kotlin
|
||||
buildTypes { release { signingConfig = signingConfigs.getByName("debug") } }
|
||||
```
|
||||
|
||||
后果有三条,且**都不报错**:
|
||||
|
||||
1. **debug 密钥是公开的**(AOSP 测试密钥,人人都有)→ 任何人都能签一个
|
||||
"同签名"的包,冒充你的应用升级;
|
||||
2. **无法上架**(Google Play 拒绝 debug 签名的包);
|
||||
3. 一旦改用正式密钥,**已装的 debug 签名版本无法覆盖升级** —— Android 只认签名一致的包。
|
||||
|
||||
第 3 条是不可逆的:**越早锁正式签名越好**。故本次一并改造。
|
||||
|
||||
## 一、密钥(keystore)
|
||||
|
||||
| 项 | 值 |
|
||||
|---|---|
|
||||
| 路径 | `~/keystores/inkwatch-release.jks`(**仓库外**) |
|
||||
| 别名 | `inkwatch` |
|
||||
| 类型 / 长度 | PKCS12 · RSA 2048 |
|
||||
| 有效期 | 至 **2054-02-01** |
|
||||
| SHA-256 指纹 | `0D:58:F8:B5:74:9B:6A:60:C8:04:2F:6F:43:33:A0:C8:47:19:2B:F3:7D:44:E5:6C:DD:47:56:F3:97:0A:D6:79` |
|
||||
| 凭据记录 | `~/keystores/inkwatch-release-CREDENTIALS.txt`(权限 600) |
|
||||
|
||||
> ⚠️⚠️ **keystore 与口令必须永久备份(离线副本、异地各一份)。**
|
||||
> 它们丢了就**永远无法更新已发布的应用** —— Android 的签名校验不可绕过,
|
||||
> 只能换包名重新上架、老用户全部流失。这是本仓库**唯一不可再生**的东西。
|
||||
|
||||
对外只公布**指纹**,绝不公布 keystore 与口令。
|
||||
|
||||
## 二、`key.properties` 契约
|
||||
|
||||
release 构建从 `client/android/key.properties` 读凭据(**已被 `.gitignore` 排除**):
|
||||
|
||||
```properties
|
||||
storeFile=/绝对路径/inkwatch-release.jks
|
||||
storePassword=...
|
||||
keyAlias=inkwatch
|
||||
keyPassword=...
|
||||
```
|
||||
|
||||
**缺这个文件时,release 构建会明确失败**(`build.gradle.kts` 末尾的 task-graph 守卫),
|
||||
报错信息里带着修复指引。
|
||||
|
||||
> 为什么不学 Flutter 模板"回落到 debug 签名":那会造出一条
|
||||
> **"看着构建成功、其实签的是公开密钥"** 的静默失败路径。
|
||||
> 本仓库多次栽在静默失败上(迁移从不执行、首次部署路径无人测),故这里宁可让它红。
|
||||
|
||||
变异验证:移走 `key.properties` → `flutter build apk --release` 在 **2 秒内失败**并打印指引;
|
||||
放回 → 构建成功。签名指纹确认为上表值(`apksigner verify --print-certs`)。
|
||||
|
||||
## 三、出包
|
||||
|
||||
```bash
|
||||
cd client
|
||||
# 通用版(主干 main)
|
||||
git checkout main
|
||||
flutter build apk --release --target-platform android-arm64 \
|
||||
--dart-define=PMON_DEFAULT_SERVER=https://<你的服务器>
|
||||
cp build/app/outputs/flutter-apk/app-release.apk \
|
||||
../dist/inkwatch-arm64-$(grep -E '^version:' pubspec.yaml | cut -d' ' -f2 | cut -d+ -f1).apk
|
||||
|
||||
# 水墨屏版(分支 feature/eink)—— 包名不同,产出也另起名
|
||||
git checkout feature/eink
|
||||
flutter build apk --release --target-platform android-arm64 ...
|
||||
cp build/app/outputs/flutter-apk/app-release.apk \
|
||||
../dist/inkwatch-eink-arm64-$(...).apk
|
||||
```
|
||||
|
||||
**验证签名**(每次发版都要做一次,5 秒):
|
||||
|
||||
```bash
|
||||
~/Library/Android/sdk/build-tools/*/apksigner verify --print-certs dist/<包>.apk | grep -E "DN:|SHA-256"
|
||||
# 期望 DN: CN=InkWatch, OU=Mobile, O=InkWatch, L=Shenzhen, ST=Guangdong, C=CN
|
||||
```
|
||||
|
||||
## 四、两个包名的分工(**刻意不同**)
|
||||
|
||||
| 版本 | 分支 | applicationId | 说明 |
|
||||
|---|---|---|---|
|
||||
| 通用版 | `main` | `com.pmon.pmon_client` | 已装机的包名,**语义上不可更改**(改了等于另一个应用) |
|
||||
| 水墨屏版 | `feature/eink` | `com.inkwatch.eink` | 刻意独立 → 两个版本**可在同一台手机上共存**、各自升级 |
|
||||
|
||||
**为什么必须分开**:两条分支的 `versionCode` 各自递增(1.0.9+9 vs 1.1.2+12)。
|
||||
若共用一个包名,它们会互相覆盖 —— 且 versionCode 小的那个会被系统当成**降级**而拒绝安装。
|
||||
两版并行的产品线(通用版给用户、水墨屏版自用/专用)必须各占一个包名。
|
||||
|
||||
> 注:`namespace`(Kotlin 包名)两版仍是 `com.pmon.pmon_client` —— 它只影响
|
||||
> R 类与源码包声明,与"装成哪个应用"无关,故不动以免大改 Kotlin 源码。
|
||||
|
||||
## 五、换签名的后果(一次性)
|
||||
|
||||
本次换正式签名后,此前用 debug 密钥装的设备(如 F50)**无法覆盖升级**,需要:
|
||||
|
||||
```bash
|
||||
adb uninstall com.pmon.pmon_client # 或水墨屏版对应包名
|
||||
adb install dist/inkwatch-arm64-<版本>.apk
|
||||
```
|
||||
|
||||
**数据不会丢**:`device_id` 自 1.0.9 起只由硬件标识(ANDROID_ID)派生,
|
||||
且服务端支持设备归并 —— 重装后重新登录并认领即可,历史帧仍在原设备名下。
|
||||
Reference in new issue
Block a user