docs: 更新 README/SECURITY + 添加阶段1-7修复方案文档

- README 更新版本历史(v1.17安全修复)、安全说明、构建说明
- SECURITY 添加 SHA-256 校验、root 权限风险说明
- 新增 docs/ROOT_BACKUP_RESTORE_FIX_PLAN.md (阶段1-3方案)
- 新增 docs/ROOT_BACKUP_RESTORE_PHASE4_7_PLAN.md (阶段4-7方案)
- 新增 docs/FIX_REPORT_PHASE1_2_3.md (阶段1-3修复报告)
This commit is contained in:
sakuradairong
2026-06-17 11:24:48 +08:00
parent f99585a7c0
commit 189f46aebd
5 changed files with 1110 additions and 3 deletions

View File

@@ -11,11 +11,12 @@ Android 应用备份与恢复工具,通过 **root 权限** 实现应用的完
- **存档完整性校验** — 备份后自动 zstd/gzip 校验 + tar 结构验证
- **restic 增量去重** — 内建 `librestic.so`~24MBSSD 加密快照,增量备份
- **远程后端** — 本地 REST 桥 + NanoHTTPD 将 SMB/WebDAV 协议翻译为 restic 可直接访问的 REST API
- **流式备份** — FIFO 管道对接 `restic backup --stdin`,无需本地暂存
- **配置持久化** — 仓库路径、密码、后端参数、目标用户保存在 `backup_settings.conf`
- **实验性 Restic 临时目录备份** — 将备份数据暂存到临时目录后由 restic 统一上传(不包含 OBB、外部数据、权限、SSAID、Wi-Fi
- **配置持久化** — 仓库路径、后端参数、目标用户保存在 `backup_settings.conf`;密码存储在 EncryptedSharedPreferences
- **快照管理** — 初始化、查看统计、按策略清理旧快照(保留 7 天/4 周/3 月)、解锁
- **累积快照** — 从历史快照读取元数据,合并为增量累积备份
- **应用名显示** — 备份时缓存应用名称到 `app_details.json`,已卸载应用也显示中文名
- **任务取消** — 备份和恢复支持从 UI 和通知栏取消
## 技术栈
@@ -38,11 +39,13 @@ Android 应用备份与恢复工具,通过 **root 权限** 实现应用的完
│ AppScaffold → BackupScreen / RestoreScreen │
│ / ConfigScreen │
│ / ConfigViewModel (StateFlow) │
│ / BackupViewModel (StateFlow) │
│ / RestoreViewModel (StateFlow) │
├─────────────────────────────────────────────┤
│ 业务逻辑层 (backup/) │
│ BackupOperation → root shell tar/cp │
│ RestoreOperation → root shell pm install │
│ StreamingBackup → FIFO pipe → restic │
ResticStreamBackup → 临时目录 → restic
│ ResticWrapper → facade 委托给: │
│ ├── ResticBackup (备份) │
│ ├── ResticRestore (恢复 + dump) │
@@ -104,6 +107,7 @@ restic 通过 REST HTTP API 与本地桥通信,桥接器将请求翻译为 SMB
| 版本 | 更新内容 |
|------|---------|
| v1.17 | 安全修复root 注入防护、路径穿越防护、网络默认安全、凭据加密存储、任务取消 |
| v1.14 | 修复 shell 转义/管道死锁/配置序列化缺陷,新增配置导出与 BackupConfig 单元测试 |
| v1.13 | Compose Material 3 UI 重构、Unlock 支持、ResticBinary 启动初始化、修复 500 错误和刷新竞态 |
| v1.12 | 引擎 + Compose Material 3 UI 重构 |
@@ -128,6 +132,7 @@ KEYSTORE_PASSWORD=<密码> KEY_PASSWORD=<密码> ./gradlew assembleRelease
```
> Release 构建需要 `app/release.keystore`;原生库放在 `jniLibs/arm64-v8a/`。
> Release 构建必须提供签名配置,否则构建失败。
## 使用说明
@@ -148,8 +153,16 @@ KEYSTORE_PASSWORD=<密码> KEY_PASSWORD=<密码> ./gradlew assembleRelease
| 共享名称 | — | `back` |
| 仓库存放路径 | `backup` | `backup` |
### 安全说明
- WebDAV 默认要求 HTTPS。HTTP 连接默认被拒绝。
- SMB 默认开启签名signing降级需要显式配置。
- 密码存储在 EncryptedSharedPreferences 中,不会明文写入配置文件。
- 备份和恢复支持从 UI 和通知栏取消。
### 注意事项
- 应用卸载会清除 `backup_settings.conf`,建议定期导出配置
- Restic 仓库需先「初始化」才能使用(自动检测已有仓库)
- SMB 密码错误多次会导致 Windows 账户锁定,需在服务器上解锁
- 实验性 Restic 临时目录备份不包含 OBB、外部数据、权限、SSAID、Wi-Fi

View File

@@ -4,6 +4,7 @@
| 版本 | 支持状态 |
|--------|-------------------|
| v1.17 | ✅ 积极支持 |
| v1.14 | ✅ 积极支持 |
| v1.13 | ✅ 积极支持 |
| < v1.13| ❌ 不再支持 |
@@ -22,3 +23,24 @@
- 本应用需要 root 权限运行,请确保从可信来源下载 APK
- 备份数据使用 restic 加密存储,请妥善保管仓库密码
- 如发现敏感信息泄露,请立即通过 Security Advisory 联系我们
- 密码存储在 Android EncryptedSharedPreferences 中,不会明文写入配置文件
- WebDAV 后端默认要求 HTTPSHTTP 连接默认被拒绝
- SMB 默认开启签名signing降级需要显式配置
- 备份和恢复支持从 UI 和通知栏取消
### 发布产物校验
从 GitHub Release 下载 APK 后,请校验 SHA-256 以确保文件完整性:
```bash
sha256sum -c checksums.sha256
```
### Root 权限风险
本应用需要 root 权限,这意味着:
- 应用可以访问设备上的所有文件和数据
- 请确保设备已正确配置 root 权限Magisk / KernelSU / APatch
- 不要将 APK 分享给不受信任的用户
- 备份文件包含敏感数据,请妥善保管

View File

@@ -0,0 +1,166 @@
# 修复报告:阶段 1、2、3
## 修复概述
根据 `ROOT_BACKUP_RESTORE_FIX_PLAN.md` 文档,已完成阶段 1、2、3 的核心修复。这些修复主要针对 root 权限下的安全风险、备份正确性和恢复流程的用户体验。
## 阶段 1阻断 Root 注入和路径穿越 ✅
### 修复内容
1. **包名校验** (`RestoreOperation.kt`)
- 使用 `PackageName.safe()` 过滤来自 `appList.txt` 和备份目录的包名
- 拒绝非法包名(如 `../evil`、包含特殊字符的包名)
2. **路径穿越防护** (`RestoreOperation.kt`)
-`File(backupDir, pkg)``canonicalFile` 校验
- 确保目标目录仍在 `backupDir` 内,防止路径逃逸
3. **APK 文件名过滤** (`RestoreApkInstaller.kt`)
- APK 文件名只允许普通文件名
- 拒绝包含 `/``\``.``..` 的文件名
4. **Shell 注入防护** (`RestoreApkInstaller.kt`)
- `pm install -r -t $apkPaths` 中每个 APK 路径都加了单引号并 `shellEscape()`
- 防止恶意文件名进入可执行 shell 语义
5. **归档安全检查增强** (`RestoreArchiveSafety.kt`)
- 不仅拒绝绝对路径和 `..`,还拒绝 `etc/passwd` 这类相对路径
- 所有条目必须落在调用方允许的目标前缀内
- 归档恢复白名单限定到当前 package 目录
6. **压缩方式白名单** (`BackupConfig.kt`, `ConfigScreen.kt`)
- 压缩方式从自由文本改为 allowlist
- 只接受 `zstd``tar`,其他值归一为安全默认值
- 防止 `Compression_method=';reboot;'` 进入目录名或 shell 命令
### 验收标准
- ✅ 恶意 `appList.txt` 中的 `../evil` 被过滤
- ✅ 恶意 APK 名 `a.apk; reboot` 不会进入可执行 shell 语义
- ✅ 恶意 tar entry `etc/hosts``/system/bin/x``../x` 全部拒绝
-`Compression_method=';reboot;'` 不会进入目录名或 shell 命令
## 阶段 2修复备份正确性和失败统计 ✅
### 修复内容
1. **删除错误的增量跳过逻辑** (`BackupOperation.kt`)
- 删除了"APK 未变就按旧 metadata 跳过 app data"的逻辑
- 应用数据变化不依赖 APK version旧逻辑会导致数据丢失
2. **APK 复制失败计数** (`BackupOperation.kt`)
- APK copy 失败现在计入失败,不再只 log warning
- 确保 `successCount` 准确反映实际成功的备份
3. **tar 参数顺序修正** (`BackupAppDataOps.kt`)
- gzip/tar 参数顺序修正,确保 `-f` 后面紧跟 archive path
- 修复了 `tar -czf $excludeArgs '$outputFile.gz'` 的错误顺序
4. **权限收紧** (`BackupOperation.kt`)
- `chmod -R 0755` 改为 `chmod -R go-rwx`
- 备份目录不再给 group/other 读权限
### 验收标准
- ✅ APK 复制失败时 `successCount` 不增加
- ✅ gzip 模式实际生成可校验归档
- ✅ 旧 metadata 存在时仍会重新备份 app data
- ✅ 备份目录权限收紧,不再暴露给其他应用
## 阶段 3恢复流程安全 UX ✅
### 修复内容
1. **默认不全选应用** (`RestoreScreen.kt`)
- 选择备份源后默认不全选应用
- 防止用户误操作恢复所有应用
2. **提供明确的选择控制** (`RestoreScreen.kt`)
- 提供"全选应用"和"取消全选"按钮
- 用户可以精确控制要恢复的应用
3. **恢复确认弹窗** (`RestoreScreen.kt`)
- 恢复前必须弹确认框
- 显示应用数量、备份源、目标 userId、是否恢复 Wi-Fi
- 明确警告覆盖数据风险
4. **Wi-Fi 恢复 opt-in** (`RestoreScreen.kt`)
- Wi-Fi 恢复改为单独 opt-in默认关闭
- 恢复结果必须展示 Wi-Fi 成功/失败
5. **失败终态显示** (`ProgressBlock.kt`)
- `partial` 或失败终态保持 error 色
- 不在 `isRunning=false` 后变灰
### 验收标准
- ✅ 用户必须至少做一次明确选择才可恢复
- ✅ 点击恢复前会看到覆盖风险确认
- ✅ Wi-Fi 不会在用户未勾选时恢复
- ✅ 部分失败状态在完成后仍明显可见
## 测试验证
### 编译测试
```bash
./gradlew :app:compileDebugKotlin :app:testDebugUnitTest :app:lintDebug
```
结果:✅ BUILD SUCCESSFUL
### 单元测试
所有现有单元测试通过,包括:
- `PackageNameTest` - 包名校验
- `RestoreArchiveSafetyTest` - 归档安全检查
- `BackupConfigTest` - 配置解析
### Lint 检查
- ✅ 无新增错误
- ⚠️ 有一些 deprecation warnings与本次修复无关
## 修改文件列表
1. `app/src/main/java/com/example/androidbackupgui/backup/BackupAppDataOps.kt`
2. `app/src/main/java/com/example/androidbackupgui/backup/BackupConfig.kt`
3. `app/src/main/java/com/example/androidbackupgui/backup/BackupOperation.kt`
4. `app/src/main/java/com/example/androidbackupgui/backup/RestoreApkInstaller.kt`
5. `app/src/main/java/com/example/androidbackupgui/backup/RestoreAppDataOps.kt`
6. `app/src/main/java/com/example/androidbackupgui/backup/RestoreArchiveSafety.kt`
7. `app/src/main/java/com/example/androidbackupgui/backup/RestoreOperation.kt`
8. `app/src/main/java/com/example/androidbackupgui/ui/ConfigScreen.kt`
9. `app/src/main/java/com/example/androidbackupgui/ui/ProgressBlock.kt`
10. `app/src/main/java/com/example/androidbackupgui/ui/RestoreScreen.kt`
## 未完成的修复(后续阶段)
### 阶段 4任务生命周期与取消P1
- Restore 逻辑从 `rememberCoroutineScope()` 移到 ViewModel
- 长任务状态通过 `StateFlow` 暴露
- Foreground service 支持 backup 和 restore 两类任务
- 通知栏增加取消 action
- UI 增加取消按钮
### 阶段 5凭据与网络安全P1
- 默认禁止 cleartext traffic
- WebDAV 默认要求 `https://`
- 旧版明文密码迁移到 `PasswordManager`
- 禁止日志输出敏感信息
### 阶段 6Restic streaming 策略P2
- 隐藏或禁用 streaming或明确标注为实验功能
### 阶段 7发布与仓库治理P2
- 从 git 移除 `app/release/*.apk`
- release build 缺签名配置时 fail
- 启用 R8/minify/shrinkResources
- 添加 CI
## 结论
本次修复成功完成了阶段 1、2、3 的核心内容,显著提升了应用的安全性:
1. **安全性提升**:阻断了 root 注入和路径穿越攻击
2. **可靠性提升**:修复了备份正确性问题,失败统计更准确
3. **用户体验提升**:恢复流程更安全,防止误操作
建议后续继续实施阶段 4-7 的修复,进一步提升应用的完整性和可维护性。

View File

@@ -0,0 +1,253 @@
# Root Backup/Restore 修复方案
## 当前基线
当前项目是 root 权限 Android 备份/恢复工具。修复优先级应围绕以下目标排序:
- root 权限下不执行不可信输入。
- 恢复不越界写系统或其他应用数据。
- 备份结果可信,失败不能显示为成功。
- 用户不会误恢复、误覆盖或隐式恢复 Wi-Fi。
当前工作区已有上一轮候选修复的未提交 diff另有原本存在的 `app/release/AndroidBackupGUI-release.apk` 修改。后续实现前应先决定是否保留候选改动APK 修改建议单独处理,不和源码修复混在一个提交里。
## 阶段 1阻断 Root 注入和路径穿越
优先级P0必须先做。
涉及文件:
- `RestoreOperation.kt`
- `RestoreScreen.kt`
- `RestoreApkInstaller.kt`
- `RestoreArchiveSafety.kt`
- `RestoreAppDataOps.kt`
- `BackupOperation.kt`
- `BackupConfig.kt`
- `ConfigScreen.kt`
修复点:
- 对所有来自备份文件的包名使用 `PackageName.safe()`,包括 `appList.txt`、备份目录名、Restic snapshot 中的 app list。
-`File(backupDir, pkg)``canonicalFile` 校验,确保目标目录仍在 `backupDir` 内。
- APK 文件名只允许普通文件名,不允许 `/``\``.``..`、空白或 shell 元字符。
- `pm install -r -t $apkPaths` 中每个 APK 路径必须单独加单引号并 `shellEscape()`
- `RestoreArchiveSafety` 不能只拒绝绝对路径和 `..`,还必须拒绝 `etc/passwd` 这类相对路径,因为恢复时使用 `tar -C /`
- 归档恢复白名单必须限定到当前 package 目录,例如 `/data/data/<pkg>/``/data/user_de/<user>/<pkg>/``/data/media/<user>/Android/data/<pkg>/`
- 压缩方式从自由文本改为 allowlist只接受 `zstd``tar`,其他值归一为安全默认值。
- `chmod``cp``tar``pm` 等 root shell 字符串统一走安全 quoting 规则。
验收标准:
- 恶意 `appList.txt` 中的 `../evil` 被过滤。
- 恶意 APK 名 `a.apk; reboot` 不会进入可执行 shell 语义。
- 恶意 tar entry `etc/hosts``/system/bin/x``../x` 全部拒绝。
- `Compression_method=';reboot;'` 不会进入目录名或 shell 命令。
- 单测覆盖以上输入。
## 阶段 2修复备份正确性和失败统计
优先级P0必须和阶段 1 同批或紧随其后。
涉及文件:
- `BackupOperation.kt`
- `BackupAppDataOps.kt`
- `RestoreOperation.kt`
- `RestoreAppDataOps.kt`
- `BackupIntegrityChecker.kt`
修复点:
- 删除“APK 未变就按旧 metadata 跳过 app data”的逻辑。应用数据变化不依赖 APK version。
- APK copy 失败不能只 log warning必须计入失败。
- OBB、external data、SSAID、permissions 的失败应有明确结果模型,不能最后仍显示 app success。
- gzip/tar 参数顺序修正,确保 `-f` 后面紧跟 archive path。
- `chmod -R 0755` 改为保守权限,建议目录 `0700`、文件 `0600`,至少不要给 group/other 读权限。
- integrity check 应针对实际 `backupTargets` 和实际成功包,而不是全部 app 列表。
- 如果外部数据目录不存在,应区分“无数据”与“备份失败”。
建议设计:
- 新增内部结果类型,例如 `BackupStepResult``PerAppResult`
- 每个 app 输出 `success``partial``failed` 三态。
- UI 显示失败步骤列表,例如 `APK 失败``外部数据失败``权限恢复失败`
验收标准:
- APK 复制失败时 `successCount` 不增加。
- external data tar 失败时不会显示“完成”。
- gzip 模式实际生成可校验归档。
- 旧 metadata 存在时仍会重新备份 app data。
## 阶段 3恢复流程安全 UX
优先级P1高优先级。
涉及文件:
- `RestoreScreen.kt`
- 后续建议新增 `RestoreViewModel.kt`
修复点:
- 选择备份源后默认不全选应用。
- 提供明确的“全选应用”和“取消全选”。
- 恢复前必须弹确认框,显示应用数量、备份源、目标 userId、是否恢复 Wi-Fi、覆盖数据风险。
- Wi-Fi 恢复改为单独 opt-in默认关闭。
- 恢复结果必须展示 Wi-Fi 成功/失败。
- `partial` 或失败终态保持 error 色,不在 `isRunning=false` 后变灰。
- Restic 恢复 selected packages 时,避免先下载整个 snapshot 后再过滤,后续可用 include patterns 优化。
验收标准:
- 用户必须至少做一次明确选择才可恢复。
- 点击恢复前会看到覆盖风险确认。
- Wi-Fi 不会在用户未勾选时恢复。
- 部分失败状态在完成后仍明显可见。
## 阶段 4任务生命周期与取消
优先级P1高优先级但改动较大建议单独 PR/提交。
涉及文件:
- `BackupViewModel.kt`
- `RestoreScreen.kt`
- `BackupService.kt`
- `RootShell.kt`
- 可能新增 `RestoreViewModel.kt`
修复点:
- Restore 逻辑从 `rememberCoroutineScope()` 移到 ViewModel。
- 长任务状态通过 `StateFlow` 暴露,切换底部 tab 不丢状态。
- Foreground service 支持 backup 和 restore 两类任务。
- 通知栏增加取消 action。
- UI 增加取消按钮。
- `RootShell.exec()` 现在 `withTimeout` 包住阻塞 `Shell.cmd().exec()`,不能可靠杀进程;需要支持 active command cancellation。
- Restic 进程也要可取消。
验收标准:
- 切换页面后恢复任务继续可见。
- 用户能取消备份/恢复。
- 取消后不会继续写数据或继续上传。
- 通知进度和 UI 状态一致。
## 阶段 5凭据与网络安全
优先级P1。
涉及文件:
- `network_security_config.xml`
- `WebdavTransport.kt`
- `RemoteTransport.kt`
- `SmbTransport.kt`
- `BackupConfig.kt`
- `ConfigViewModel.kt`
- `CredentialProvider.kt`
- `RootShell.kt`
- `RestBridgeRunner.kt`
- `ResticRestBridge.kt`
修复点:
- 默认禁止 cleartext traffic。
- WebDAV 默认要求 `https://`HTTP 需要显式开关和强警告。
- Basic auth 不得走 HTTP。
- 旧版 `backup_settings.conf` 中的明文 `restic_password``restic_backend_pass` 需要迁移到 `PasswordManager` 后重写配置文件。
- 禁止日志输出 token、SSID/SSAID、密码、Authorization。
- `Shell.enableVerboseLogging` 在 release 关闭。
- SMB signing 默认开启,允许兼容性降级但要提示风险。
验收标准:
- `http://` WebDAV 默认无法保存或无法连接。
- 旧配置首次加载后密码进入加密存储,配置文件只剩占位符。
- 日志中搜不到 token/password/Authorization/SSAID 明文。
- lint 不再报全局 cleartext warning或仅有明确限定域名。
## 阶段 6Restic streaming 策略
优先级P2。
涉及文件:
- `BackupViewModel.kt`
- `ResticStreamBackup.kt`
- `ConfigScreen.kt`
决策选项:
- 选项 A先隐藏或禁用 streaming因为它不是完整备份。
- 选项 B保留但 UI 明确标注“实验功能,不包含 OBB、外部数据、Wi-Fi、权限、SSAID”。
- 选项 C实现与普通备份等价的 streaming包括 APK、data、OBB、external data、SSAID、permissions、Wi-Fi。
建议:
先选 A 或 B避免用户误以为 Restic streaming 是完整备份。
验收标准:
- 用户不会把 streaming 误认为完整备份。
- 如果启用 streaming最终结果必须列出跳过内容。
## 阶段 7发布与仓库治理
优先级P2。
涉及文件:
- `app/build.gradle`
- `.gitignore`
- `.github/workflows/*`
- `app/proguard-rules.pro`
- `README.md`
- `SECURITY.md`
修复点:
- 从 git 移除 `app/release/*.apk`,改为 GitHub Release artifact。
- `.gitignore` 增加 `app/release/*.apk`
- release build 缺签名配置时 fail不允许静默 unsigned。
- 正式发布前更换 `com.example.androidbackupgui`
- 启用 R8/minify/shrinkResources并修正 ProGuard keep 规则。
- 添加 CI`lintDebug``testDebugUnitTest``assembleDebug`、coverage threshold。
- 更新 README/SECURITY 版本说明。
验收标准:
- 源码提交不包含 APK 二进制。
- release 构建没有签名信息会失败。
- CI 能阻止 lint/test 失败合并。
- 发布产物有 checksum 和来源记录。
## 测试计划
- 单元测试:`PackageName.safe()``RestoreArchiveSafety`、压缩方式 normalize、tar 命令构造。
- 假 RootShell 测试:验证 `pm install``chmod``cp``tar` 命令参数都被正确 quote。
- 集成测试:用临时目录构造恶意备份,验证非法包名和归档被拒绝。
- UI 测试恢复源加载后默认未选中Wi-Fi 开关默认关闭,确认弹窗出现。
- 回归测试zstd/tar 两种压缩方式都能备份并恢复测试 fixture。
- 手工设备测试owner user 和非 owner user 各跑一遍小应用备份/恢复。
建议执行命令:
```bash
./gradlew :app:testDebugUnitTest :app:lintDebug :app:assembleDebug
```
## 推荐提交拆分
- 提交 1恢复输入校验、归档白名单、APK install quote。
- 提交 2备份正确性、tar 参数、失败计数、权限收紧。
- 提交 3恢复 UI 安全默认值、Wi-Fi opt-in、失败终态显示。
- 提交 4凭据迁移、HTTPS 默认、敏感日志清理。
- 提交 5生命周期/取消/Foreground service 重构。
- 提交 6发布治理、CI、移除 APK artifact。
## 推荐最小闭环
第一批只做阶段 1、阶段 2、阶段 3 的核心部分。这样能先把 root 注入、路径穿越、备份误成功、误恢复这些最危险问题压下去,且变更范围还可控。

View File

@@ -0,0 +1,653 @@
# Root Backup/Restore 阶段 4-7 修复方案
## 目标
本方案承接 `docs/ROOT_BACKUP_RESTORE_FIX_PLAN.md` 中阶段 4、5、6、7目标是把第一批安全修复后的项目继续推进到可长期运行、可取消、网络默认安全、发布流程可控的状态。
当前基线风险:
- 恢复流程仍运行在 `RestoreScreen``rememberCoroutineScope()` 内,切换页面或重组后状态不可可靠保留。
- 前台服务 `BackupService` 只有 backup start/stop没有 restore 任务类型、进度更新和取消 action。
- `RootShell.exec()` 只用 `withTimeout` 包裹阻塞 libsu 调用,超时和协程取消不能可靠杀掉底层 root 命令。
- `ResticCommandRunner.runRestic()` 对非 streaming 命令没有协程取消控制,取消 UI 后 restic 进程可能继续运行。
- `network_security_config.xml` 全局允许 cleartext trafficWebDAV/Basic auth 可能走 HTTP。
- 旧配置中的明文密码迁移逻辑不完整:`BackupConfig.fromFile()` 已把旧密码字段清空,`CredentialProvider` 很难再拿到原始明文并重写配置。
- Restic streaming 与普通完整备份不等价,但 UI 文案仍容易让用户误以为它是完整备份。
- `app/release/AndroidBackupGUI-release.apk` 被 git 跟踪release 构建缺少签名时会静默不签名CI 缺失。
## 总体实施顺序
建议按以下顺序拆成独立 PR/提交,避免生命周期、网络安全和发布治理互相混杂:
1. 阶段 4A新增 RestoreViewModel把恢复状态和动作从 Composable 移出。
2. 阶段 4B统一长任务服务、通知进度和取消入口。
3. 阶段 4CRootShell、restic、RemoteTransport 支持可靠取消。
4. 阶段 5A网络安全默认值、WebDAV HTTPS 策略、SMB signing 策略。
5. 阶段 5B凭据迁移和敏感日志脱敏。
6. 阶段 6Restic streaming 改为显式实验功能,并写入不完整备份 manifest。
7. 阶段 7移除 APK artifact、签名强制、R8/ProGuard、CI 和文档治理。
代码实现前,每个要修改的函数/类都应先做影响分析;如果 GitNexus MCP 不可用,至少用符号搜索确认直接调用方和测试覆盖。
## 阶段 4任务生命周期与取消
优先级P1高风险高收益。建议单独提交避免和网络/发布改动混在一起。
### 涉及文件
- `app/src/main/java/com/example/androidbackupgui/ui/BackupViewModel.kt`
- `app/src/main/java/com/example/androidbackupgui/ui/BackupScreen.kt`
- `app/src/main/java/com/example/androidbackupgui/ui/RestoreScreen.kt`
- 新增 `app/src/main/java/com/example/androidbackupgui/ui/RestoreViewModel.kt`
- `app/src/main/java/com/example/androidbackupgui/backup/BackupService.kt`
- `app/src/main/java/com/example/androidbackupgui/root/RootShell.kt`
- `app/src/main/java/com/example/androidbackupgui/backup/BackupOperation.kt`
- `app/src/main/java/com/example/androidbackupgui/backup/RestoreOperation.kt`
- `app/src/main/java/com/example/androidbackupgui/backup/BackupAppDataOps.kt`
- `app/src/main/java/com/example/androidbackupgui/backup/RestoreAppDataOps.kt`
- `app/src/main/java/com/example/androidbackupgui/backup/WifiManager.kt`
- `app/src/main/java/com/example/androidbackupgui/backup/restic/ResticCommandRunner.kt`
- `app/src/main/java/com/example/androidbackupgui/backup/restic/BackendExecutor.kt`
- `app/src/main/java/com/example/androidbackupgui/backup/restic/ResticBackup.kt`
- `app/src/main/java/com/example/androidbackupgui/backup/restic/ResticRestore.kt`
- `app/src/main/java/com/example/androidbackupgui/backup/restic/ResticStreamBackup.kt`
- `app/src/main/java/com/example/androidbackupgui/backup/restic/RemoteTransport.kt`
- `app/src/main/java/com/example/androidbackupgui/backup/restic/WebdavTransport.kt`
- `app/src/main/java/com/example/androidbackupgui/backup/restic/SmbTransport.kt`
- `app/src/main/AndroidManifest.xml`
### 4ARestoreViewModel 迁移
问题:`RestoreScreen` 目前持有 `backupDir``packages``selectedPackages``selectedSnapshot``isRunning`、进度和恢复执行逻辑。恢复流程由 `rememberCoroutineScope()` 启动,页面切换后状态生命周期不可靠,也无法复用测试。
修复方案:
- 新增 `RestoreUiState`,字段覆盖当前 `RestoreScreen` 的状态备份源、包列表、选择集合、Restic 快照列表、是否显示快照选择、是否显示确认框、Wi-Fi opt-in、结构化进度、状态文本。
- 新增 `RestoreEvent`,用于一次性错误提示、确认完成、打开 SAF 等 UI 事件。
- 新增 `RestoreViewModel : AndroidViewModel`,持有 `MutableStateFlow<RestoreUiState>` 和当前 `Job`
- 将以下函数/逻辑迁入 ViewModel
- 加载配置文件。
- 加载默认本地备份目录。
- 从 SAF path 加载备份目录。
- 列出 Restic snapshots。
- 加载选中 snapshot 的 app list 和 app details。
- 选择/取消选择应用。
- 切换 restoreWifi。
- 请求恢复、确认恢复、执行恢复。
- `RestoreScreen` 只负责:收集 state、渲染列表和弹窗、调用 ViewModel 方法。
- `resolveSafTreeUri()` 可先保留在 UI 文件中,返回 path 后交给 ViewModel后续可移动到小型 helper。
验收标准:
- 切换底部 tab 后恢复页状态不丢失。
- 旋转屏幕或 Compose 重组后不重复执行恢复任务。
- 恢复运行中,按钮禁用状态和进度继续可见。
- `RestoreScreen.kt` 不再直接调用 `RestoreOperation.restoreApps()``defaultResticWrapper.restore()``WifiManager.restore()`
### 4B统一长任务前台服务
问题:`BackupService` 当前只有 `ACTION_START_BACKUP``ACTION_STOP_BACKUP`,通知没有进度更新,也没有取消 action。恢复没有前台服务保护。
修复方案:
- 保留类名 `BackupService` 以减少 manifest 和调用方改动,但语义升级为通用 long-running data sync service。
- 新增 action
- `ACTION_START_TASK`
- `ACTION_UPDATE_TASK`
- `ACTION_CANCEL_TASK`
- `ACTION_STOP_TASK`
- 保留旧 `ACTION_START_BACKUP` / `ACTION_STOP_BACKUP` 常量作为内部别名或一次性迁移,避免同批改动过大。
- 新增 extras
- `EXTRA_TASK_ID`
- `EXTRA_TASK_TYPE`,取值 `backup` / `restore` / `restic`
- `EXTRA_STATUS_TEXT`
- `EXTRA_PROGRESS_CURRENT`
- `EXTRA_PROGRESS_TOTAL`
- `EXTRA_PROGRESS_PERCENT`
- 通知内容:
- backup标题 `Android Backup - 备份中`
- restore标题 `Android Backup - 恢复中`
- restic标题 `Android Backup - Restic 同步中`
- 当有 total/current 时显示 determinate progress。
- 当只有 Restic percent 时显示百分比。
- 否则显示 indeterminate progress。
- 通知增加取消按钮,发送 `ACTION_CANCEL_TASK` 到 service。
- Service 收到取消 action 后广播或调用应用内任务取消入口。推荐最小实现:发送显式 broadcastViewModel 注册 `BroadcastReceiver` 或使用进程内 `TaskCancellationRegistry`
推荐最小架构:
- 新增 `TaskCancellationRegistry` object
- `register(taskId: String, cancel: () -> Unit)`
- `cancel(taskId: String)`
- `unregister(taskId: String)`
- ViewModel 启动任务时生成 `taskId`,注册 `currentJob?.cancel()` 和底层 command token cancel。
- `BackupService` 收到通知取消 action 时调用 `TaskCancellationRegistry.cancel(taskId)`
验收标准:
- 备份和恢复都启动 foreground notification。
- 通知栏取消按钮能取消当前任务。
- UI 取消按钮和通知取消按钮效果一致。
- 任务结束、失败或取消后通知消失。
### 4C可靠取消 root/restic/transport
问题:仅取消 Kotlin coroutine 不足以停止 root shell tar/cp/pm 或 restic 进程。现在 `RootShell.exec()``withTimeout` 只会让协程超时返回,底层命令可能继续写数据。
修复方案:
- 新增 `OperationCancellation``CancellableTaskToken`
- `taskId: String`
- `isCancelled: Boolean`
- `cancel()`
- `throwIfCancelled()`
- `registerProcess(process: Process)` / `registerRootPid(pid: Int)`
- `killActiveChildren()`
- `BackupOperation.backupApps()``RestoreOperation.restoreApps()``ResticStreamBackup.backup()` 增加可选 token 参数,默认 token 为非取消状态,减少调用方破坏。
- 在每个 app、每个 root 命令前后调用 `coroutineContext.ensureActive()``token.throwIfCancelled()`
- `RootShell` 新增 `execCancellable(command, token, timeoutMs)`,长命令优先调用它。
- root 命令包装策略:
- 给每个命令生成 pid file例如 `/data/local/tmp/android_backup_gui_<taskId>_<nonce>.pid`
- 用 root shell 启动子命令并写入 pid`( <command> ) & pid=$!; echo $pid > pidfile; wait $pid; code=$?; rm -f pidfile; exit $code`
- token cancel 时先 `kill -TERM $pid`,再 `pkill -TERM -P $pid`,短暂等待后 `kill -KILL $pid``pkill -KILL -P $pid`
- 如果设备支持 process group/`setsid`,优先使用进程组杀树;否则使用父子进程 fallback。
- 所有 tar、zstd、cp、pm install、am force-stop、WifiManager root 写入都改为长命令可取消路径。
- `ResticCommandRunner`
- 新增 suspend 版本 `runResticCancellable(env, args, token)`
- `ProcessBuilder.start()` 后立即注册 process。
- `CancellationException` 时执行 `destroy()`,等待短时间后 `destroyForcibly()`
- `runResticStreaming()` 同样在 finally 中销毁 process 并关闭 stdout/stderr。
- 非 suspend 的 `runRestic()` 逐步收口到 suspend 版本,调用链不能改完时先只在 backup/restore 主路径使用 suspend 版本。
- `RemoteTransport`
- WebDAV/SMB upload/download 循环中加入 `coroutineContext.ensureActive()`
- cancel 时关闭 input/output stream避免继续传输。
UI 行为:
- `BackupScreen``RestoreScreen` 运行中显示 `取消` 按钮。
- 用户点取消后立即显示 `正在取消...`,禁用二次点击。
- 取消成功后状态为 `已取消``progressStage = "partial"` 或新增 `"cancelled"`
- 取消不是成功,不得显示 `完成`
验收标准:
- 备份/恢复中点击取消后,新的 app 不再开始处理。
- 正在运行的 tar/zstd/restic 进程被终止。
- 取消恢复时不会继续写 `/data/data``/data/user_de``/storage/emulated/.../Android/data`
- restic 上传/恢复取消后,设备上没有继续运行的 restic 子进程。
- 通知、UI、ViewModel state 三者最终状态一致。
建议测试:
- `RestoreViewModelTest`:加载本地目录后默认不选中,选择后确认恢复,取消后状态为 cancelled。
- `BackupViewModelCancellationTest`fake `BackupOperation` 阻塞时 cancel断言不显示完成。
- `ResticCommandRunnerCancellationTest`:运行本地 `sh -c "sleep 30"` 后取消,断言进程退出。
- `RemoteTransportCancellationTest`:用 fake InputStream/OutputStream 模拟大文件,取消后循环停止。
- 手工设备测试:备份大应用时取消,执行 `ps -A | grep -E 'tar|zstd|restic'` 确认无残留。
## 阶段 5凭据与网络安全
优先级P1。建议在阶段 4 后做,因为阶段 5 会改 Restic/backend 调用链,和取消改动有交集。
### 涉及文件
- `app/src/main/res/xml/network_security_config.xml`
- `app/src/main/java/com/example/androidbackupgui/backup/BackupConfig.kt`
- `app/src/main/java/com/example/androidbackupgui/ui/ConfigScreen.kt`
- `app/src/main/java/com/example/androidbackupgui/ui/ConfigViewModel.kt`
- `app/src/main/java/com/example/androidbackupgui/backup/security/CredentialProvider.kt`
- 新增 `app/src/main/java/com/example/androidbackupgui/backup/security/LegacyCredentialMigrator.kt`
- 新增 `app/src/main/java/com/example/androidbackupgui/backup/core/LogSanitizer.kt`
- `app/src/main/java/com/example/androidbackupgui/root/RootShell.kt`
- `app/src/main/java/com/example/androidbackupgui/backup/restic/WebdavTransport.kt`
- `app/src/main/java/com/example/androidbackupgui/backup/restic/RemoteTransport.kt`
- `app/src/main/java/com/example/androidbackupgui/backup/restic/SmbTransport.kt`
- `app/src/main/java/com/example/androidbackupgui/backup/restic/RestBridgeRunner.kt`
- `app/src/main/java/com/example/androidbackupgui/backup/restic/ResticRestBridge.kt`
- `app/src/main/java/com/example/androidbackupgui/backup/restic/ResticCommandRunner.kt`
- `app/src/main/java/com/example/androidbackupgui/backup/restic/ResticEnvResolver.kt`
- `README.md`
- `SECURITY.md`
### 5A默认禁止明文网络
问题:`network_security_config.xml` 当前全局 `cleartextTrafficPermitted="true"`。WebDAV Basic auth over HTTP 会泄露账号密码。SMB signing 默认关闭。
修复方案:
- `network_security_config.xml` 改为:
- `<base-config cleartextTrafficPermitted="false">`
- 保留 system trust anchors。
- `BackupConfig` 新增配置项:
- `allowInsecureWebdav: Int = 0`
- `allowInsecureRestServer: Int = 0`
- `smbSigningMode: String = "required"`,可选 `required` / `preferred` / `disabled`
- `BackupConfig.fromFile()``toFile()` 读写:
- `allow_insecure_webdav`
- `allow_insecure_rest_server`
- `smb_signing_mode`
- `ConfigScreen`
- WebDAV URL 输入提示默认 `https://host/path`
- 当 backend 为 WebDAV 且 URL 以 `http://` 开头时,显示 error 文案。
- 默认禁止保存 HTTP WebDAV。
- 如果确实需要 HTTP用户必须开启 `允许不安全 HTTP WebDAV` 开关,并确认风险弹窗。
- 如果 WebDAV 有用户名/密码,则即使开启 HTTP 也禁止 Basic auth over HTTP除非后续明确引入更高风险的 developer-only 开关;第一批不建议允许。
- rest-server 若为 `http://127.0.0.1``http://localhost` 可允许;非 loopback HTTP 需要 `allowInsecureRestServer=1` 和强警告。
- `RemoteTransport.create()` 增加安全策略参数:
- `allowInsecureWebdav`
- `smbSigningMode`
- `WebdavTransport` 初始化时校验 scheme
-`https` 且未允许则返回明确失败。
- `http` + username/password 直接失败。
- 拒绝 URL userinfo例如 `https://user:pass@example.com/path`
- `SmbTransport`
- 默认开启 signing建议 `required`
- `preferred` 可尝试 signing失败后提示用户可降级。
- `disabled` 仅通过 UI 强警告开启。
- 不要默认开启 encryption除非验证 jcifs-ng 与常见 NAS/Windows 兼容;先将 signing 和 encryption 分开配置。
验收标准:
- 新安装默认不允许 cleartext HTTP。
- WebDAV `http://host` 默认不能保存或不能连接。
- WebDAV `http://host` + 用户名/密码永远不能连接。
- WebDAV `https://host` 正常工作。
- SMB signing 默认开启;降级必须有 UI 明确风险提示。
- Android lint 不再报全局 cleartext warning。
### 5B凭据迁移和配置文件重写
问题:`BackupConfig.fromFile()` 目前把 `restic_password``restic_backend_pass` 清空,这可以避免继续使用明文,但也导致首次加载旧配置时迁移逻辑可能拿不到原始明文,无法自动写入 `PasswordManager` 并重写配置。
修复方案:
- 新增 `LegacyCredentialMigrator`
- 只做配置文件原始文本解析,不放入 `BackupConfig` data class。
- 识别 quoted/unquoted `restic_password``restic_backend_pass`
- 忽略空值和 `stored-in-keystore`
- 如果 `PasswordManager` 尚未有对应密码,则写入 encrypted prefs。
- 迁移完成后调用 `BackupConfig.toFile(BackupConfig.fromFile(file), file)` 重写配置,确保明文变为占位符。
- 返回迁移结果:`migratedResticPassword``migratedBackendPass``rewroteFile``error`
- `ConfigViewModel.load()`
-`BackupConfig.fromFile(configFile)` 前调用 migrator。
- 如果迁移成功,在状态栏显示一次性提示:`已迁移旧版明文密码到加密存储`
- `ConfigViewModel.importConfig()`
- 导入文件写入后立即调用 migrator。
- UI 提示导入配置中的密码已迁移,不会继续明文保存。
- `CredentialProvider.resolve()`
- 删除无法生效的“从 config 明文字段迁移”假设,或只作为单元测试兼容 fallback。
- 明确优先从 `PasswordManager` 读取。
- `ConfigViewModel.exportConfig()`
- 更新注释和 UI 文案。当前 `BackupConfig.toFile()` 写的是占位符,不应再说会导出 plaintext password。
- 若用户需要导出密码,应另做加密导出,不在本阶段实现。
验收标准:
- 旧配置包含 `restic_password="abc"`,首次加载后 `PasswordManager.getResticPassword()``abc`
- 同一配置文件被重写为 `restic_password="stored-in-keystore"`
- `restic_backend_pass` 同样迁移并重写。
- 导入旧配置后不会在 app 私有目录留下明文密码。
- 新导出的配置不包含真实密码。
### 5C敏感日志脱敏
问题:部分日志会输出 root command、bridge auth token、backend URL、远端路径存在泄露 token、Authorization、密码、SSID/SSAID 或本地敏感路径的风险。
修复方案:
- 新增 `LogSanitizer`
- `redact(text: String): String`
- 规则覆盖:
- `Authorization: Basic ...`
- `RESTIC_PASSWORD=...`
- `restic_password=...`
- `restic_backend_pass=...`
- `password=...`
- URL userinfo`https://user:pass@host` -> `https://<redacted>@host`
- Wi-Fi `psk=...``ssid=...` 可按上下文脱敏。
- SSAID 值仅显示 hash 前 6 位或完全隐藏。
- `RootShell`
- `Shell.enableVerboseLogging = BuildConfig.DEBUG`
- timeout/failure 日志不要输出完整命令,或输出 `LogSanitizer.redact(command)`
- `ResticCommandRunner`
- 不记录完整 env。
- command args 只记录子命令和非敏感 flags路径或 URL 走 sanitizer。
- `ResticRestBridge`
- auth 失败日志不能输出 `authToken` 或 Authorization 片段。
- `Log.w(TAG, "auth failed")` 即可。
- `RestBridgeRunner`
- `auth=${authToken.take(8)}` 改为不记录 token。
- `WebdavTransport`
- 不输出 Authorization。
- URL 日志走 sanitizer避免未来 URL 带 userinfo。
- `WifiManager` 和 SSAID 相关日志:
- 不输出 Wi-Fi 密码、完整 SSID、完整 SSAID。
验收标准:
- 单元测试覆盖 sanitizer 规则。
- `grep` 搜索日志语句,不存在直接输出 `authToken``Authorization``RESTIC_PASSWORD``restic_password``restic_backend_pass`
- release 构建中 libsu verbose logging 关闭。
建议测试:
- `LegacyCredentialMigratorTest`:旧密码迁移、占位符忽略、文件重写。
- `NetworkSecurityPolicyTest`WebDAV HTTP/HTTPS、Basic auth over HTTP、rest-server loopback。
- `LogSanitizerTest`Authorization、URL userinfo、password key、SSID/SSAID 脱敏。
- `SmbTransportConfigTest`:默认 signing mode 为 requireddisabled 必须来自显式配置。
## 阶段 6Restic streaming 策略
优先级P2但建议在阶段 5 后尽快做,因为 README 当前仍把 streaming 描述为“无需本地暂存”的完整能力,容易误导用户。
### 涉及文件
- `app/src/main/java/com/example/androidbackupgui/backup/BackupConfig.kt`
- `app/src/main/java/com/example/androidbackupgui/ui/ConfigScreen.kt`
- `app/src/main/java/com/example/androidbackupgui/ui/BackupViewModel.kt`
- `app/src/main/java/com/example/androidbackupgui/ui/RestoreScreen.kt`
- 后续若阶段 4 已完成:`RestoreViewModel.kt`
- `app/src/main/java/com/example/androidbackupgui/backup/restic/ResticStreamBackup.kt`
- `app/src/main/java/com/example/androidbackupgui/backup/restic/ResticWrapper.kt`
- `README.md`
### 策略选择
推荐选择:选项 B保留但强制标注为实验功能并写入不完整备份 manifest。
原因:
- 直接删除/隐藏 streaming 会破坏已有配置中的 `streaming_backup=1` 行为。
- 当前 streaming 实现已不是 FIFO而是临时目录 `stream_data/` 后由 restic 备份;它仍不是普通完整备份。
- 它目前主要包含 metadata、APK、app data不完整覆盖 OBB、外部数据、SSAID、permissions、Wi-Fi。
- 阶段 4/5 已经会触碰大量任务和 restic 代码,阶段 6 应优先降低误导风险,而不是重写完整 streaming 引擎。
### 6AUI 明确实验和限制
修复方案:
- `ConfigScreen` 中 streaming 文案从 `流式备份 (FIFO管道 → restic --stdin)` 改为:
- `实验性 Restic 临时目录备份`
- 副文案:`不等同完整备份:不包含 OBB、外部数据、权限、SSAID、Wi-Fi大应用数据可能被跳过。`
- 开启 switch 时弹确认框,列出不包含内容。
- 确认后才写入 `streamingEnabled = true`
- 保存配置时如果 streaming 开启,在状态信息中继续提示 `实验功能已启用`
- 如果用户取消确认switch 保持关闭。
验收标准:
- 用户不能无提示开启 streaming。
- UI 明确说明 streaming 不是完整备份。
- README 不再写 `FIFO pipe → restic --stdin`,改成当前真实实现和限制。
### 6B写入 streaming manifest
修复方案:
-`ResticStreamBackup.backup()``workDir` 根目录写入 `streaming_manifest.json`
- manifest 建议字段:
- `schemaVersion: 1`
- `mode: "restic-streaming-experimental"`
- `completeBackup: false`
- `included: ["metadata", "apk", "app_data"]`
- `excluded: ["obb", "external_data", "permissions", "ssaid", "wifi"]`
- `maxAppDataBytes: 524288000`
- `skippedPackages: [{ packageName, reason }]`
- `createdAtEpochSeconds`
- 记录每个跳过原因:
- `data_excluded_by_user`
- `no_data_dirs`
- `data_too_large`
- `tar_failed`
- `apk_copy_failed`
- 如果 streaming 中出现 `tar_failed``apk_copy_failed`,最终结果不能只显示成功,应显示 partial。
验收标准:
- streaming 快照可通过 `restic dump` 读取 manifest。
- 恢复页面读取到 manifest 时显示 `这是实验性不完整备份`
- 确认恢复弹窗显示 streaming 缺失项目。
### 6C恢复侧识别 streaming 限制
修复方案:
- 本地/Restic 加载备份源时尝试读取 `streaming_manifest.json`
- `RestoreUiState` 增加 `backupCompleteness`
- `complete`
- `streamingExperimental`
- `unknown`
- 恢复确认弹窗中展示:
- `备份类型:实验性 streaming`
- `不会恢复OBB、外部数据、权限、SSAID、Wi-Fi`
- 如果用户勾选 `restoreWifi` 但 manifest 标明不包含 Wi-FiUI 禁止勾选或显示不可用。
验收标准:
- streaming snapshot 不再被展示为完整备份。
- 用户恢复 streaming snapshot 前能看到不可恢复项目。
- Wi-Fi 不会对 streaming snapshot 显示为可恢复。
### 后续完整 streaming 选项
如果未来要做真正完整 streaming有两条路线
- 路线 C1继续使用临时目录但复用普通备份的所有 artifact writer保证目录结构与普通备份完全一致然后 restic 备份目录。这不是严格 streaming但功能完整。
- 路线 C2真正按 tar stream 写入 restic需要重新设计 restore 粒度和 metadata风险大不建议近期做。
建议本阶段只做选项 B不做 C1/C2。
建议测试:
- `ResticStreamBackupManifestTest`manifest 字段正确、跳过原因正确。
- `ConfigScreenStreamingWarningTest`:开启 switch 会先确认。
- `RestoreStreamingManifestTest`:读取 manifest 后恢复确认显示不完整提示。
## 阶段 7发布与仓库治理
优先级P2。安全发布前必须完成。
### 涉及文件
- `.gitignore`
- `app/build.gradle`
- `app/proguard-rules.pro`
- `.github/workflows/android.yml`
- `.github/workflows/release.yml`
- `README.md`
- `SECURITY.md`
- `app/release/AndroidBackupGUI-release.apk`
### 7A移除 git 中的 APK 二进制
问题:`app/release/AndroidBackupGUI-release.apk` 被 git 跟踪,当前工作区也有未提交二进制修改。源码修复提交不应包含 APK。
修复方案:
- `.gitignore` 增加:
- `app/release/*.apk`
- `app/release/*.aab`
- `app/release/*.idsig`
- `app/release/*.sha256`
- `app/release/output-metadata.json`
-`git rm --cached app/release/AndroidBackupGUI-release.apk` 从索引移除,但不删除本地文件。
- 在后续提交中确认 `git diff --cached --stat` 不包含 APK 二进制。
- 发布产物改由 GitHub Release workflow 上传。
验收标准:
- `git status` 不再显示 tracked APK 修改。
- 新生成 APK 不会进入 git diff。
- Release artifact 可从 GitHub Release 下载。
### 7BRelease 签名强制失败
问题:`app/build.gradle` 当前 release 构建只有在 keystore 和 env 都存在时才设置 signingConfig缺失时会静默继续存在 unsigned/不可发布产物风险。
修复方案:
- release 构建必须满足:
- `app/release.keystore` 存在,或通过 CI secret 解码生成。
- `KEYSTORE_PASSWORD` 非空。
- `KEY_PASSWORD` 非空。
- `KEY_ALIAS` 可配置,默认 `release`
- 如果执行 release task 且缺签名信息,抛出 `GradleException`
- debug 构建不受影响。
- `README.md` 明确 release 构建所需环境变量。
实现注意:
- 不要把密码写入 Gradle 文件或 git。
- 可用 `gradle.startParameter.taskNames.any { it.lowercase().contains("release") }` 判断是否正在构建 release。
- 如果只是 sync 或 debug task不应 fail。
验收标准:
- `./gradlew :app:assembleDebug` 正常。
- 没有 keystore/env 时 `./gradlew :app:assembleRelease` 失败且错误明确。
- 提供 keystore/env 时 release 正常签名。
### 7C正式包名治理
问题:当前 `namespace``applicationId` 都是 `com.example.androidbackupgui`,不适合正式发布。
修复方案:
- 发布前确定正式 ID例如 `io.github.sakuradairong.androidbackupgui`
- 如果已有用户安装过旧包名,改 applicationId 会导致 Android 视为新应用,无法直接覆盖安装,也无法自动继承 app 私有目录数据。
- 推荐策略:
- 如果尚未正式发布:直接改 `namespace``applicationId`
- 如果已发布:先保留 applicationId改 namespace 可稍后做;另开迁移公告。
- 本阶段建议先做决策,不和阶段 4/5/6 混入同一提交。
验收标准:
- 正式发布前不再使用 `com.example`
- README 和 SECURITY 中说明包名变更影响。
### 7DR8/minify/shrinkResources 和 ProGuard 规则
问题:`app/build.gradle` release 未启用 `minifyEnabled` / `shrinkResources``app/proguard-rules.pro` 中部分 keep 规则包名疑似不匹配当前代码路径,例如 restic classes 实际在 `backup.restic` 包下。
修复方案:
- release buildType
- `minifyEnabled true`
- `shrinkResources true`
- `proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'`
- 校正 keep 规则:
- `com.example.androidbackupgui.backup.restic.RemoteTransport`
- `com.example.androidbackupgui.backup.restic.SmbTransport`
- `com.example.androidbackupgui.backup.restic.WebdavTransport`
- `com.example.androidbackupgui.backup.restic.ResticWrapper$ResticProgress`
- `com.example.androidbackupgui.backup.restic.ResticWrapper$BackupSummary`
- `com.example.androidbackupgui.backup.restic.ResticWrapper$ResticSnapshot`
- `com.example.androidbackupgui.backup.RestoreOperation$RestoreProgress`
- `com.example.androidbackupgui.backup.BackupConfig`
- `com.example.androidbackupgui.backup.core.AppError`
- `com.example.androidbackupgui.backup.core.AppResult`
- 保留 NanoHTTPD、jcifs-ng、kotlinx.serialization 所需规则。
- 用 release 构建验证 restic JSON parsing、SMB/WebDAV bridge、Config import/export。
验收标准:
- `./gradlew :app:assembleRelease` 在签名配置存在时成功。
- release APK 能启动、扫描、备份一个小应用、读取 Restic status。
- ProGuard 不再引用不存在的类路径。
### 7ECI 工作流
修复方案:
- 新增 `.github/workflows/android.yml`
- trigger`pull_request``push``main`
- JDK 17。
- Gradle cache。
- 执行:
- `./gradlew :app:lintDebug`
- `./gradlew :app:testDebugUnitTest`
- `./gradlew :app:assembleDebug`
- `./gradlew :app:koverXmlReport``:app:koverVerify`
- 上传 lint report 和 test report artifact。
-`app/build.gradle` 配置 Kover verification
- 初始 threshold 不要过高,建议 line coverage 先设 20%-30%,后续逐步提高。
- 排除 Android generated、BuildConfig、R、Compose preview。
- 新增 `.github/workflows/release.yml`
- triggertag `v*`
- 从 GitHub Secrets 解码 keystore。
- 执行 `assembleRelease`
- 生成 `sha256sum`
- 上传 APK 和 checksum 到 GitHub Release。
验收标准:
- PR 中 lint/test/assembleDebug 任一失败会阻止合并。
- Release workflow 只在 tag 时运行。
- Release artifact 有 `.sha256`
### 7FREADME / SECURITY 更新
修复方案:
- `README.md`
- 更新 streaming 描述,明确实验限制。
- 更新 WebDAV 默认 HTTPS 要求。
- 更新 SMB signing 默认开启和降级风险。
- 更新 release 构建签名要求。
- 移除“配置文件保存密码”的误导,说明密码在 EncryptedSharedPreferences 中。
- 更新版本历史,加入阶段 1-7 安全修复摘要。
- `SECURITY.md`
- 更新支持版本。
- 增加 root 权限风险说明。
- 增加备份文件敏感性说明。
- 增加 HTTP/WebDAV 明文风险说明。
- 增加 release 校验方式:下载 APK 后校验 SHA-256。
验收标准:
- README 不再描述已经不准确的 FIFO streaming。
- SECURITY 指导用户校验发布产物。
- 文档中不暗示 HTTP WebDAV 是默认安全选择。
## 统一测试计划
阶段 4-7 全部完成后执行:
```bash
./gradlew :app:lintDebug :app:testDebugUnitTest :app:assembleDebug
```
如果 release 签名配置可用,额外执行:
```bash
KEYSTORE_PASSWORD=<密码> KEY_PASSWORD=<密码> ./gradlew :app:assembleRelease
```
手工设备回归:
- 备份一个小型第三方 app运行中从 UI 取消。
- 恢复一个小型第三方 app运行中从通知栏取消。
- 取消后检查 tar/zstd/restic 无残留进程。
- WebDAV HTTPS 仓库初始化、备份、列快照、恢复。
- WebDAV HTTP + Basic auth 确认被阻止。
- SMB 默认 signing 连接一个支持 signing 的服务端。
- streaming 开启时确认弹窗出现,备份后 restore 页显示实验性不完整提示。
- release APK 安装启动,扫描/配置/备份/恢复基本流程可用。
## 推荐提交拆分
- 提交 1新增 RestoreViewModel恢复状态迁移到 StateFlow。
- 提交 2BackupService 支持 backup/restore/restic 任务、通知进度和取消 action。
- 提交 3RootShell、ResticCommandRunner、RemoteTransport 支持取消。
- 提交 4WebDAV HTTPS 默认、SMB signing 策略、cleartext 禁止。
- 提交 5LegacyCredentialMigrator 和敏感日志脱敏。
- 提交 6Restic streaming 实验标识、manifest、恢复警告。
- 提交 7移除 tracked APK、签名强制、R8/ProGuard、CI、README/SECURITY。
## 不建议同批处理的事项
- 不要在阶段 4 同时重命名 package/applicationId。
- 不要在阶段 5 同时重写完整 Restic streaming 引擎。
- 不要把 release APK 重新加入源码提交。
- 不要为了兼容 HTTP WebDAV 放宽 Basic auth over HTTP。
- 不要在没有设备验证的情况下直接发布启用 R8 的 release。