diff --git a/.github/workflows/legado.jks b/.github/workflows/legado.jks deleted file mode 100644 index 89ea725fd..000000000 Binary files a/.github/workflows/legado.jks and /dev/null differ diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fbef50854..822ea764b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -82,7 +82,6 @@ jobs: sed -i "/^def version =/c\\def version = \"$VERSION\"" app/build.gradle if [[ "$VARIANT" == "releaseA" ]]; then sed -i "s/applicationIdSuffix '.release'/applicationIdSuffix '.releaseA'/" app/build.gradle - sed -i 's/\.release/\.releaseA/g' app/google-services.json fi - name: 配置 Gradle diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2a7fdc83a..b8d4e7e63 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -6,8 +6,6 @@ on: - master paths: - '**' - # - '!**/assets/**' - # - '!**.md' - '!**/ISSUE_TEMPLATE/**' - '!**/modules/web/**' pull_request: @@ -20,259 +18,50 @@ on: - completed workflow_dispatch: +permissions: + contents: read + concurrency: - group: ${{ github.workflow }}-${{ github.ref }} + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true jobs: - - prepare: - runs-on: ubuntu-latest - if: ${{ !startsWith(github.event.head_commit.message, 'Merge pull request') }} - outputs: - version: ${{ steps.set-ver.outputs.version }} - versionL: ${{ steps.set-ver.outputs.versionL }} - lanzou: ${{ steps.check.outputs.lanzou }} - telegram: ${{ steps.check.outputs.telegram }} - steps: - - id: set-ver - run: | - echo "version=$(date -d "8 hour" -u +3.%y.%m%d%H)" >> $GITHUB_OUTPUT - echo "versionL=$(date -d "8 hour" -u +3.%y.%m%d%H%M)" >> $GITHUB_OUTPUT - - id: check - run: | - if [ ${{ secrets.LANZOU_ID }} ]; then - echo "lanzou=yes" >> $GITHUB_OUTPUT - fi - if [ ${{ secrets.BOT_TOKEN }} ]; then - echo "telegram=yes" >> $GITHUB_OUTPUT - fi - - build: - needs: prepare - strategy: - matrix: - product: [ app ] - type: [ release, releaseA ] - fail-fast: false + verify: + name: Android Debug 验证 + if: github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' runs-on: ubuntu-latest - env: - product: ${{ matrix.product }} - type: ${{ matrix.type }} - VERSION: ${{ needs.prepare.outputs.version }} - VERSIONL: ${{ needs.prepare.outputs.versionL }} steps: - - uses: actions/checkout@v4 + - name: 检出代码 + uses: actions/checkout@v4 with: fetch-depth: 0 - - name: Set up JDK 17 + + - name: 配置 JDK 17 uses: actions/setup-java@v4 with: - distribution: 'temurin' + distribution: temurin java-version: 17 - - name: Clear 18PlusList.txt - run: | - echo "清空18PlusList.txt" - echo "">$GITHUB_WORKSPACE/app/src/main/assets/18PlusList.txt - - name: Release Apk Sign - run: | - echo "给apk增加签名" - cp $GITHUB_WORKSPACE/.github/workflows/legado.jks $GITHUB_WORKSPACE/app/legado.jks - sed '$a\RELEASE_STORE_FILE=./legado.jks' $GITHUB_WORKSPACE/gradle.properties -i - sed '$a\RELEASE_KEY_ALIAS=legado' $GITHUB_WORKSPACE/gradle.properties -i - sed '$a\RELEASE_STORE_PASSWORD=gedoor_legado' $GITHUB_WORKSPACE/gradle.properties -i - sed '$a\RELEASE_KEY_PASSWORD=gedoor_legado' $GITHUB_WORKSPACE/gradle.properties -i - - - name: Set up Gradle - uses: gradle/actions/setup-gradle@v4 - - name: Build With Gradle - continue-on-error: true + - name: 清理成人内容列表 + shell: bash run: | - if [ ${{ env.type }} == 'release' ]; then - typeName="原包名" - else - typeName="共存" - sed "s/'.release'/'.releaseA'/" $GITHUB_WORKSPACE/app/build.gradle -i - sed 's/.release/.releaseA/' $GITHUB_WORKSPACE/app/google-services.json -i - fi - echo "统一版本号" - sed "/def version/c def version = \"${{ env.VERSION }}\"" $GITHUB_WORKSPACE/app/build.gradle -i - echo "开始${{ env.product }}$typeName构建" - chmod +x gradlew - ./gradlew assemble${{ env.product }}release --build-cache --parallel --daemon --warning-mode all - echo "修改APK文件名" - mkdir -p ${{ github.workspace }}/apk/ - for file in `ls ${{ github.workspace }}/app/build/outputs/apk/*/*/*.apk`; do - mv "$file" ${{ github.workspace }}/apk/legado_${{ env.product }}_${{ env.VERSIONL }}_$typeName.apk - done - echo "移动mapping文件" - mkdir -p ${{ github.workspace }}/mapping/ - for file in `ls ${{ github.workspace }}/app/build/outputs/mapping/*/mapping.txt`; do - mv "$file" ${{ github.workspace }}/mapping/mapping.txt - done + : > app/src/main/assets/18PlusList.txt - - name: Move Missing Rules Files - run: | - echo "移动missing_rules.txt文件" - mkdir -p ${{ github.workspace }}/mapping/ - for file in `ls ${{ github.workspace }}/app/build/outputs/mapping/*/missing_rules.txt`; do - mv "$file" ${{ github.workspace }}/mapping/missing_rules.txt - done - - - name: Upload Missing Rules File To Artifact - uses: actions/upload-artifact@v4 - with: - name: legado.${{ env.product }}.${{ env.type }}.mapping.missing_rules - if-no-files-found: ignore - path: ${{ github.workspace }}/mapping/missing_rules.txt + - name: 配置 Gradle + uses: gradle/actions/setup-gradle@v4 - - name: Check Build production + - name: 运行单元测试并构建 Debug APK + shell: bash run: | - if [ ! -d ${{ github.workspace }}/apk ]; then - echo "Build production not found! Check gradle logs." - exit 1 - fi - cd ${{ github.workspace }}/apk/ - if [ ! -e legado_*.apk ]; then - echo "Build production not found! Check gradle logs." - exit 1 - fi - - - name: Upload App To Artifact - uses: actions/upload-artifact@v4 - with: - name: legado.${{ env.product }}.${{ env.type }} - if-no-files-found: ignore - path: ${{ github.workspace }}/apk/*.apk + set -euo pipefail + ./gradlew :app:testAppDebugUnitTest :app:assembleAppDebug \ + --build-cache \ + --no-daemon \ + --warning-mode all - - name: Upload Mapping File To Artifact + - name: 上传 Debug APK uses: actions/upload-artifact@v4 with: - name: legado.${{ env.product }}.${{ env.type }}.mapping - if-no-files-found: ignore - path: ${{ github.workspace }}/mapping/mapping.txt - - prerelease: - needs: [ prepare, build ] - if: github.event_name != 'pull_request' && github.repository == 'gedoor/legado' - runs-on: ubuntu-latest - env: - VERSION: ${{ needs.prepare.outputs.version }} - steps: - - uses: actions/checkout@v4 - - uses: actions/download-artifact@v4 - with: - path: apk/ - - - working-directory: apk/ - run: | - mv */*.apk . - rm -rf */ - - for file in `ls *.apk`; do - if [[ "$file" == *原包名* ]]; then - mv "$file" $(echo $file | sed s/原包名/release/) - else - mv "$file" $(echo $file | sed s/共存/releaseA/) - fi - done - - - name: Delete Pre-Release - run: | - if gh release view beta &>/dev/null; then - gh release delete beta -y - fi - env: - GH_TOKEN: ${{ github.token }} - - - name: Create or update beta tag - uses: richardsimko/update-tag@v1 - with: - tag_name: beta - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Publish Pre-Release - uses: ncipollo/release-action@v1 - with: - name: legado_app_${{ env.VERSION }} - tag: "beta" - body: | - 此版本为测试版,签名与正式版不同,可能存在不稳定情况,升级前请务必备份好数据。 - releaseA 为共存版本,可同时安装使用,功能没有区别。 - prerelease: true - artifacts: ${{ github.workspace }}/apk/*.apk - - lanzou: - needs: [ prepare, build ] - if: ${{ github.event_name != 'pull_request' && needs.prepare.outputs.lanzou == 'yes' }} - runs-on: ubuntu-latest - env: - # 登录蓝奏云后在控制台运行document.cookie - ylogin: ${{ secrets.LANZOU_ID }} - phpdisk_info: ${{ secrets.LANZOU_PSD }} - # 蓝奏云里的文件夹ID(阅读3测试版:2670621) - LANZOU_FOLDER_ID: ${{ secrets.LANZOU_FOLDER_ID }} - #蓝奏云分享链接 - LANZOU_URL: ${{ secrets.LANZOU_URL }} - steps: - - uses: actions/checkout@v4 - - uses: actions/download-artifact@v4 - with: - path: apk/ - - working-directory: apk/ - run: mv */*.apk . ;rm -rf */ - - name: Upload To Lanzou - continue-on-error: true - run: | - path="$GITHUB_WORKSPACE/apk/" - python3 $GITHUB_WORKSPACE/.github/scripts/lzy_web.py "$path" "$LANZOU_FOLDER_ID" - echo "[$(date -u -d '+8 hour' '+%Y.%m.%d %H:%M:%S')] 分享链接: $LANZOU_URL" - - test_Branch: - needs: [ prepare, build ] - runs-on: ubuntu-latest - if: ${{ github.event_name != 'pull_request' && github.actor == 'gedoor' }} - steps: - - uses: actions/checkout@v4 - - uses: actions/download-artifact@v4 - with: - path: apk/ - - working-directory: apk/ - run: mv */*.apk . ;rm -rf */ - - name: Push To "test" Branch - run: | - cd $GITHUB_WORKSPACE/apk/ - git init - git checkout -b test - git config --global user.name "github-actions[bot]" - git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" - git remote add origin "https://${{ github.actor }}:${{ secrets.ACTIONS_TOKEN }}@github.com/${{ github.actor }}/release" - git add *.apk - git commit -m "${{ needs.prepare.outputs.versionL }}" - git push -f -u origin test - - telegram: - needs: [ prepare, build ] - if: ${{ github.event_name != 'pull_request' && needs.prepare.outputs.telegram == 'yes' }} - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/download-artifact@v4 - with: - path: apk/ - - working-directory: apk/ - run: | - for file in `ls */*.apk`; do - mv "$file" "$(echo "$file"|sed -e 's#.*\/##g' -e "s/_/ /g" -e 's/legado/阅读/')" - done - rm -rf */ - - name: Post to channel - uses: xireiki/channel-post@v1 - with: - chat_id: ${{ secrets.CHANNEL_ID }} - bot_token: ${{ secrets.BOT_TOKEN }} - context: "#阅读 #Legado #Beta ${{ needs.prepare.outputs.versionL }}" - path: apk/* - method: sendFile + name: legado-app-debug + if-no-files-found: error + path: app/build/outputs/apk/app/debug/*.apk diff --git a/.gitignore b/.gitignore index 95dc44a22..37fcb7fe9 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,13 @@ node_modules/ package-lock.json .idea/ .signing/ +# Firebase 客户端配置由仓库所有者按需提供 +/app/google-services.json +# 本地签名材料不得提交 +*.jks +*.keystore +*.p12 +*.pfx # OpenSpec 为本地智能体生成的上游工作流指令 .agents/skills/ # Kotlin 2.0 diff --git a/app/build.gradle b/app/build.gradle index 976fbdfe3..9f1c8c3c8 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -4,14 +4,11 @@ plugins { // id 'kotlin-parcelize' // //id 'kotlin-kapt' // id 'com.google.devtools.ksp' -// id "com.google.gms.google-services" - alias libs.plugins.android.application alias libs.plugins.kotlin.android alias libs.plugins.kotlin.parcelize alias libs.plugins.room alias libs.plugins.ksp - alias libs.plugins.google.services } apply from: 'download.gradle' @@ -277,11 +274,6 @@ dependencies { //noinspection GradleDependency,GradlePackageUpdate implementation(libs.hutool.crypto) - //firebase, 崩溃统计和性能统计 - implementation platform(libs.firebase.bom) - implementation libs.firebase.analytics - implementation libs.firebase.perf - implementation libs.glide.recyclerview //LeakCanary, 内存泄露检测 diff --git a/app/google-services.json b/app/google-services.json deleted file mode 100644 index fcbc11f0d..000000000 --- a/app/google-services.json +++ /dev/null @@ -1,151 +0,0 @@ -{ - "project_info": { - "project_number": "453392274790", - "firebase_url": "https://legado-fca69.firebaseio.com", - "project_id": "legado-fca69", - "storage_bucket": "legado-fca69.appspot.com" - }, - "client": [ - { - "client_info": { - "mobilesdk_app_id": "1:453392274790:android:c4eac14b1410eec5f624a7", - "android_client_info": { - "package_name": "io.legado.app.debug" - } - }, - "oauth_client": [ - { - "client_id": "453392274790-hnbpatpce9hbjiggj76hgo7queu86atq.apps.googleusercontent.com", - "client_type": 3 - } - ], - "api_key": [ - { - "current_key": "AIzaSyD90mfNLhA7cAzzI9SonpSz5mrF5BnmyJA" - } - ], - "services": { - "appinvite_service": { - "other_platform_oauth_client": [ - { - "client_id": "453392274790-hnbpatpce9hbjiggj76hgo7queu86atq.apps.googleusercontent.com", - "client_type": 3 - } - ] - } - } - }, - { - "client_info": { - "mobilesdk_app_id": "1:453392274790:android:c1481c1c3d3f51eff624a7", - "android_client_info": { - "package_name": "io.legado.app.release" - } - }, - "oauth_client": [ - { - "client_id": "453392274790-trrgennt5njr1lhil1sgtf0ogcgd38fo.apps.googleusercontent.com", - "client_type": 1, - "android_info": { - "package_name": "io.legado.app.release", - "certificate_hash": "fd67dba87b7b761631266f19ddde249054aac5c1" - } - }, - { - "client_id": "453392274790-hnbpatpce9hbjiggj76hgo7queu86atq.apps.googleusercontent.com", - "client_type": 3 - } - ], - "api_key": [ - { - "current_key": "AIzaSyD90mfNLhA7cAzzI9SonpSz5mrF5BnmyJA" - } - ], - "services": { - "appinvite_service": { - "other_platform_oauth_client": [ - { - "client_id": "453392274790-hnbpatpce9hbjiggj76hgo7queu86atq.apps.googleusercontent.com", - "client_type": 3 - } - ] - } - } - }, - { - "client_info": { - "mobilesdk_app_id": "1:453392274790:android:b891abd2331577dff624a7", - "android_client_info": { - "package_name": "io.legado.play.release" - } - }, - "oauth_client": [ - { - "client_id": "453392274790-f8sjn6ohs72rg1dvp0pdvk42nkq54p0k.apps.googleusercontent.com", - "client_type": 1, - "android_info": { - "package_name": "io.legado.play.release", - "certificate_hash": "00819ace9891386e535967cbafd6a88f3797bd5b" - } - }, - { - "client_id": "453392274790-hnbpatpce9hbjiggj76hgo7queu86atq.apps.googleusercontent.com", - "client_type": 3 - } - ], - "api_key": [ - { - "current_key": "AIzaSyD90mfNLhA7cAzzI9SonpSz5mrF5BnmyJA" - } - ], - "services": { - "appinvite_service": { - "other_platform_oauth_client": [ - { - "client_id": "453392274790-hnbpatpce9hbjiggj76hgo7queu86atq.apps.googleusercontent.com", - "client_type": 3 - } - ] - } - } - }, - { - "client_info": { - "mobilesdk_app_id": "1:453392274790:android:b891abd2331577dff624a7", - "android_client_info": { - "package_name": "io.legado.play.debug" - } - }, - "oauth_client": [ - { - "client_id": "453392274790-f8sjn6ohs72rg1dvp0pdvk42nkq54p0k.apps.googleusercontent.com", - "client_type": 1, - "android_info": { - "package_name": "io.legado.play.debug", - "certificate_hash": "00819ace9891386e535967cbafd6a88f3797bd5b" - } - }, - { - "client_id": "453392274790-hnbpatpce9hbjiggj76hgo7queu86atq.apps.googleusercontent.com", - "client_type": 3 - } - ], - "api_key": [ - { - "current_key": "AIzaSyD90mfNLhA7cAzzI9SonpSz5mrF5BnmyJA" - } - ], - "services": { - "appinvite_service": { - "other_platform_oauth_client": [ - { - "client_id": "453392274790-hnbpatpce9hbjiggj76hgo7queu86atq.apps.googleusercontent.com", - "client_type": 3 - } - ] - } - } - } - ], - "configuration_version": "1" -} \ No newline at end of file diff --git a/app/src/main/assets/privacyPolicy.md b/app/src/main/assets/privacyPolicy.md index abf1cddb7..09861ef3c 100644 --- a/app/src/main/assets/privacyPolicy.md +++ b/app/src/main/assets/privacyPolicy.md @@ -1,5 +1,4 @@ -* 本应用没有服务端,不收集任何用户信息,只采用了Google Firebase收集崩溃报告和性能报告. +* 本应用没有服务端,不收集任何用户信息,也未集成Firebase等统计或性能监测服务。 * 本应用网络同步和备份采用webDav协议,由用户自己提供同步服务. * 存储权限用来打开本地文件和本地备份恢复. -* 其它一些权限是Google Firebase需要. -* 本应用为开源软件,内置js引擎,因书源调用js发生的任何问题由用户自行承担. \ No newline at end of file +* 本应用为开源软件,内置js引擎,因书源调用js发生的任何问题由用户自行承担. diff --git a/build.gradle b/build.gradle index 8e9216dab..dddff5287 100644 --- a/build.gradle +++ b/build.gradle @@ -19,14 +19,11 @@ plugins { //id 'org.jetbrains.kotlin.android' version "$kotlin_version" apply false //id 'com.google.devtools.ksp' version "$kotlin_version-$ksp_version" apply false //id "de.undercouch.download" version "5.5.0" apply false - //id "com.google.gms.google-services" version "4.4.0" apply false - alias libs.plugins.android.application apply false alias libs.plugins.android.library apply false alias libs.plugins.kotlin.android apply false alias libs.plugins.kotlin.parcelize apply false alias libs.plugins.ksp apply false - alias libs.plugins.google.services apply false alias libs.plugins.room apply false alias libs.plugins.download apply false diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0a9a04a1b..060a1bb46 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -70,9 +70,6 @@ room = "2.7.1" recyclerview = "1.2.0" #noinspection GradleDependency viewpager2 = "1.0.0" -#noinspection GradleDependency -firebaseBom = "33.2.0" - [libraries] activity-activity = { module = "androidx.activity:activity", version.ref = "activity" } @@ -92,11 +89,6 @@ core-ktx = { module = "androidx.core:core-ktx", version.ref = "core" } androidx-espresso-core = { module = "androidx.test.espresso:espresso-core", version = "3.6.1" } -firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebaseBom" } -firebase-analytics = { module = "com.google.firebase:firebase-analytics" } -firebase-perf = { module = "com.google.firebase:firebase-perf" } - - fragment-fragment = { module = "androidx.fragment:fragment", version.ref = "fragment" } fragment-ktx = { module = "androidx.fragment:fragment-ktx", version.ref = "fragment" } fragment-testing = { module = "androidx.fragment:fragment-testing", version.ref = "fragment" } @@ -208,11 +200,9 @@ kotlin-sam = { id = "org.jetbrains.kotlin.plugin.sam.with.receiver", version.ref kotlin-lombok = { id = "org.jetbrains.kotlin.plugin.lombok", version.ref = "kotlin" } kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" } -google-services = { id = "com.google.gms.google-services", version = "4.4.2" } - ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } room = { id = "androidx.room", version.ref = "room" } #protobuf = { id = "com.google.protobuf", version = "0.9.4" } -download = { id = "de.undercouch.download", version = "5.6.0" } \ No newline at end of file +download = { id = "de.undercouch.download", version = "5.6.0" } diff --git a/openspec/changes/harden-independent-fork-security/.openspec.yaml b/openspec/changes/harden-independent-fork-security/.openspec.yaml new file mode 100644 index 000000000..d7bc0110d --- /dev/null +++ b/openspec/changes/harden-independent-fork-security/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-08-10 diff --git a/openspec/changes/harden-independent-fork-security/design.md b/openspec/changes/harden-independent-fork-security/design.md new file mode 100644 index 000000000..9b9186acf --- /dev/null +++ b/openspec/changes/harden-independent-fork-security/design.md @@ -0,0 +1,79 @@ +## 背景 + +详见 `proposal.md`。当前 `:app` 应用了 Google Services 插件并依赖 Firebase Analytics 与 Performance,跟踪树包含上游 `google-services.json`。旧 `Test Build` 还会复制仓库内的 `legado.jks`、写入硬编码密码并允许 Gradle 构建步骤失败后继续执行;新的手动 `Release Build` 已经具备通过 GitHub Actions Secrets 恢复和校验独立签名的路径。 + +本变更跨越 Android 构建配置、隐私说明和 GitHub Actions,且涉及签名身份与遥测数据边界,因此需要在实现前固定安全决策。它不触及运行时业务模块、最低 SDK 21、Room 数据、规则格式、导入接口或备份。 + +## 目标与非目标 + +**目标:** + +- 让独立仓库的本地与 CI Debug 构建不依赖上游 Firebase 项目。 +- 让 `Test Build` 成为不接触发布签名的失败即停止验证入口。 +- 让手动 `Release Build` 成为唯一正式签名发布入口,并保持缺少 Secrets 时主动失败。 +- 防止常见 Firebase 配置与签名文件再次误提交。 +- 使用聚焦修改,避免覆盖另一 session 的无关工作区改动。 + +**非目标:** + +- 不重写已经公开的 Git 历史,也不声称历史凭据恢复保密。 +- 不代替上游所有者撤销 Firebase Key 或旧签名。 +- 不创建新的 Firebase 项目,不保留 Analytics 或 Performance 的占位集成。 +- 不处理 Dependabot PR、依赖升级、分支 Ruleset 或网页端构建流程。 +- 不改变应用数据、协议和阅读功能行为。 + +## 技术决策 + +### 1. 完整移除 Firebase,而不是隐藏客户端 Key + +从根插件声明、`:app` 插件和依赖、版本目录、工作流及跟踪树中移除 Google Services 与 Firebase 条目,并同步更新隐私政策。Firebase Android 客户端 Key 最终会进入 APK,把 `google-services.json` 改存 Actions Secret 只能隐藏仓库文件,不能建立真正的保密边界;继续使用上游受限 Key 又要求本仓库依赖一个无权管理的外部项目,因此两种替代方案均不采用。 + +如果未来需要遥测,应通过新的 OpenSpec 变更接入仓库所有者控制的项目,并重新评估隐私说明、包名和签名证书限制。 + +### 2. `Test Build` 收敛为 Debug 验证工作流 + +删除公开签名步骤、发布变体矩阵、映射文件处理以及上游专用的预发布、蓝奏云、测试分支和 Telegram 分发任务。验证入口执行 `:app:testAppDebugUnitTest` 与 `:app:assembleAppDebug`,并可把 Debug APK 作为 GitHub Actions artifact 留给维护者下载。 + +选择 Debug 构建而不是未签名 Release,是因为 Debug APK 可直接用于开发验证且使用独立应用标识,不会被误认为正式发布包。选择固定验证入口而不是临时生成 CI Release 签名,是为了避免产生证书身份不稳定但外观看似正式的 APK。 + +工作流保留与 Android 相关的现有路径过滤,Web-only 变更继续由独立 Web 工作流负责。本次不把状态检查加入 Ruleset;待新验证工作流在实际 PR 上稳定运行后再单独启用。 + +### 3. 构建与测试失败必须向上传播 + +验证命令在同一脚本中以严格错误处理运行,删除构建步骤的 `continue-on-error`。单元测试或 assemble 任一失败都会终止 job,artifact 上传仅在前置命令成功时发生。 + +上传外部渠道不再属于此工作流,因此无需保留“上传失败但构建成功”的特殊语义。 + +### 4. 手动正式发布继续使用 Secrets + +保留 `.github/workflows/release.yml` 的 `workflow_dispatch`、签名 Secrets 完整性检查、临时恢复签名文件和 `release`、`releaseA` 两变体。仅移除已失效的 `google-services.json` 替换逻辑;签名文件仍在 runner 内生成并且不进入 artifact 或仓库。 + +正式签名文件使用仓库现有的 `RELEASE_KEY_STORE`、`RELEASE_STORE_PASSWORD`、`RELEASE_KEY_ALIAS`、`RELEASE_KEY_PASSWORD` Secrets 契约。缺失或校验失败时保持 fail-closed,不增加公开签名回退。 + +### 5. 忽略规则覆盖本地服务配置与签名格式 + +在 `.gitignore` 中加入 `/app/google-services.json` 以及 `*.jks`、`*.keystore`、`*.p12`、`*.pfx`。这些规则只防止新文件误提交;已经跟踪的旧文件需要在本变更中显式删除,历史暴露则通过告警说明而不是破坏性改写解决。 + +### 6. 实施时采用重叠检测 + +开始每个文件修改前重新读取 `git status --short` 和目标 diff。若另一 session 修改了同一文件,暂停该文件的应用,先比较意图再做点选合并;不覆盖、不回退、不批量格式化另一 session 的工作。 + +## 风险与权衡 + +- [移除 Firebase 后不再获得 Analytics 和 Performance 数据] → 这是独立仓库当前期望的隐私边界;未来需要时使用自有项目重新接入。 +- [旧 Key、旧签名和密码仍存在于历史与上游] → 将其视为永久公开,不再用于本仓库身份;不以历史重写冒充轮换。 +- [Debug artifact 不能覆盖安装正式版] → 文件名和工作流说明明确标注 Debug,只用于开发验证;正式安装包仅由手动发布工作流产生。 +- [Fail-closed 暴露既有测试或构建失败] → 先在本地运行相同命令并修复真实问题,再考虑把检查设为分支必需项。 +- [删除版本目录条目可能与依赖更新产生冲突] → 仅删除本变更已不再引用的 Firebase 和 Google Services 条目,不调整任何其他版本。 +- [另一 session 同时修改相同文件] → 逐文件复查共享工作区,出现重叠立即暂停并保留双方改动。 + +## 迁移计划 + +1. 记录共享工作区状态,确认另一 session 的改动文件。 +2. 删除 Firebase 插件、依赖、客户端配置和工作流引用,更新隐私政策与忽略规则。 +3. 删除旧签名文件与硬编码签名步骤,把 `Test Build` 收敛为 Debug 验证。 +4. 保留并检查 `Release Build` 的 Secrets 签名路径,删除其中的 Firebase 文件变换。 +5. 运行聚焦单元测试、Debug 构建、OpenSpec 严格校验和补丁检查。 +6. 通过受保护分支的 Pull Request 合并;确认当前提交不再触发上游 Firebase 配置的使用后,按事实说明关闭历史 Secret scanning 告警。 + +若需要回滚运行时变化,可以恢复 Firebase 集成,但只能接入仓库所有者控制的新项目;不得恢复公开 `legado.jks` 或硬编码密码。CI 调整可独立回退到上一版本的验证命令,但正式发布仍必须保持 Secrets-only。 diff --git a/openspec/changes/harden-independent-fork-security/proposal.md b/openspec/changes/harden-independent-fork-security/proposal.md new file mode 100644 index 000000000..92762973d --- /dev/null +++ b/openspec/changes/harden-independent-fork-security/proposal.md @@ -0,0 +1,31 @@ +## 为什么 + +当前仓库继承了上游 Firebase 客户端配置、公开签名文件及允许构建失败继续执行的测试工作流。作为独立开发仓库继续沿用这些资产,会把本仓库构建与上游遥测项目和公开签名身份绑定,并可能让失败的构建显示为成功,因此需要在首次独立发布前完成隔离和加固。 + +## 变更内容 + +- 移除继承的 Firebase Google Services 配置、Gradle 插件与 Analytics、Performance 依赖,并同步清理工作流引用和隐私政策说明。 +- 删除仓库中的旧 `legado.jks` 及测试工作流内的硬编码签名配置;测试构建不得使用正式发布签名材料。 +- 将旧测试工作流收敛为面向 Pull Request 和 `master` 的验证流程,使用 Debug 构建产物,不再执行上游专用的预发布、蓝奏云、测试分支和 Telegram 分发。 +- 让单元测试或 Gradle 构建失败直接导致工作流失败,不再通过 `continue-on-error` 掩盖失败。 +- 保留现有手动正式发布工作流,并继续要求通过 GitHub Actions Secrets 注入本仓库自己的签名材料。 +- 在忽略规则中排除本地 Firebase 配置和签名文件,避免再次误提交。 +- **非目标**:不重写 Git 历史、不轮换或撤销上游凭据、不新增本仓库 Firebase 项目、不处理现有 Dependabot PR、不升级依赖、不调整已创建的 GitHub 分支规则。 + +## 能力 + +### 新增能力 + +- `fork-build-security`:规定独立仓库的遥测隔离、签名材料边界、验证工作流失败语义和正式发布签名要求。 + +### 修改能力 + +无。当前主规范中没有对应能力。 + +## 影响 + +- 受影响范围:`app/` 的 Gradle 配置与隐私政策、根 Gradle 插件声明、`gradle/libs.versions.toml`、`.gitignore`、`.github/workflows/test.yml`、`.github/workflows/release.yml` 及旧签名文件。 +- 用户可感知变化:独立构建不再初始化 Firebase Analytics 或 Performance,也不再向上游 Firebase 项目发送相关数据;正式 APK 仍由手动发布工作流生成。 +- 兼容性:不修改最低 SDK、应用数据、Room 结构、书源与订阅源规则、JSON、导入 URI、备份或网页端接口,不需要数据迁移。 +- 安全边界:历史提交和上游仓库中的公开材料不会因本变更恢复保密;旧签名必须视为已公开,后续正式发布只能使用本仓库所有者控制的新签名。 +- 验收条件:跟踪树中不存在继承的 Firebase 配置和签名二进制;Debug 构建无需 Firebase 文件即可通过;验证工作流遇到测试或构建失败时返回失败;正式发布工作流在签名 Secrets 缺失时明确失败,并且不会退回公开签名。 diff --git a/openspec/changes/harden-independent-fork-security/specs/fork-build-security/spec.md b/openspec/changes/harden-independent-fork-security/specs/fork-build-security/spec.md new file mode 100644 index 000000000..65af88bd0 --- /dev/null +++ b/openspec/changes/harden-independent-fork-security/specs/fork-build-security/spec.md @@ -0,0 +1,61 @@ +## Purpose + +本能力用于确保复制后的独立仓库不再依赖上游遥测项目或公开签名身份,并让验证构建、正式发布和敏感材料管理具有可检查、失败即停止的安全边界。 + +## ADDED Requirements + +### Requirement: 独立构建与上游遥测隔离 +系统 MUST 在不提供上游 Firebase 客户端配置的情况下完成受支持的 Debug 构建,并且构建产物不得初始化上游 Firebase Analytics 或 Performance。 + +#### Scenario: 干净检出执行 Debug 构建 +- **WHEN** 开发者从干净检出执行项目规定的 Debug 构建 +- **THEN** 构建无需 `app/google-services.json` 即可完成 +- **AND** 产物不包含本仓库继承的上游 Firebase 初始化配置 + +#### Scenario: 查看应用隐私说明 +- **WHEN** 用户查看应用内隐私政策 +- **THEN** 文档不得声称当前构建使用已经移除的 Firebase 统计或性能服务 + +### Requirement: 验证工作流不使用发布签名 +Pull Request 和 `master` 分支的验证工作流 MUST 运行测试并构建 Debug 产物,且 MUST NOT 读取仓库内签名二进制、硬编码签名密码或正式发布 Secrets。 + +#### Scenario: Pull Request 触发验证 +- **WHEN** Pull Request 的改动命中 Android 验证范围 +- **THEN** 工作流执行单元测试和 Debug 构建 +- **AND** 工作流不生成或分发使用正式应用标识和发布签名的 APK + +#### Scenario: 验证命令失败 +- **WHEN** 单元测试或 Gradle 构建命令返回非零状态 +- **THEN** 验证工作流 MUST 返回失败 +- **AND** 后续步骤不得把该次验证报告为成功 + +### Requirement: 正式发布仅接受受控签名 +正式发布工作流 MUST 只使用 GitHub Actions Secrets 注入的本仓库签名材料,并且 MUST NOT 回退到仓库文件或公开签名。 + +#### Scenario: 发布 Secrets 完整 +- **WHEN** 仓库所有者手动触发正式发布且所需签名 Secrets 全部存在 +- **THEN** 工作流验证签名材料后构建受签名的正式变体 + +#### Scenario: 发布 Secrets 缺失 +- **WHEN** 任一必需签名 Secret 缺失或签名材料无法验证 +- **THEN** 正式发布工作流在构建前明确失败 +- **AND** 不生成使用其他签名的替代发布包 + +### Requirement: 仓库不跟踪继承的敏感材料 +当前跟踪树 MUST NOT 包含继承的 Google Services 配置、旧签名二进制或硬编码签名密码,并且忽略规则 MUST 覆盖常见本地签名文件和 Firebase 客户端配置。 + +#### Scenario: 检查版本控制跟踪树 +- **WHEN** 维护者检查当前提交所跟踪的配置与签名文件 +- **THEN** 不存在继承的 `app/google-services.json` 和旧 `legado.jks` +- **AND** 工作流中不存在对应的硬编码签名凭据 + +#### Scenario: 开发者生成本地签名材料 +- **WHEN** 开发者在工作区生成常见格式的本地签名文件或 Firebase 配置 +- **THEN** 这些文件默认不会被 Git 纳入新的提交 + +### Requirement: 安全加固保持应用数据兼容 +本变更 MUST NOT 修改最低 SDK、Room 数据库、书源和订阅源规则、导入 URI、JSON 字段或备份格式。 + +#### Scenario: 现有用户升级安全加固构建 +- **WHEN** 用户从兼容签名的既有版本升级到完成安全加固的版本 +- **THEN** 应用持久化数据和兼容性敏感接口无需迁移即可继续使用 diff --git a/openspec/changes/harden-independent-fork-security/tasks.md b/openspec/changes/harden-independent-fork-security/tasks.md new file mode 100644 index 000000000..fde6295d6 --- /dev/null +++ b/openspec/changes/harden-independent-fork-security/tasks.md @@ -0,0 +1,27 @@ +## 1. 协作与范围保护 + +- [x] 1.1 在实现前检查 `git status --short`、当前 diff 和活动 OpenSpec 变更,记录另一 session 的改动文件;发现目标文件重叠时先点选合并,不覆盖用户改动。 +- [x] 1.2 确认本次仅处理继承的 Firebase、旧签名材料和 Android 验证工作流,不修改 Dependabot PR、分支 Ruleset、依赖版本或业务代码。 + +## 2. 移除上游 Firebase 集成 + +- [x] 2.1 从根 Gradle、`:app` 和版本目录中移除 Google Services 插件及 Firebase Analytics、Performance 依赖,不调整其他依赖版本。 +- [x] 2.2 删除跟踪的 `app/google-services.json`,并在 `.gitignore` 中加入 Firebase 配置和常见签名文件格式的忽略规则。 +- [x] 2.3 删除所有 GitHub Actions 中对 `google-services.json` 的读取或包名替换逻辑。 +- [x] 2.4 更新应用内隐私政策,准确说明当前构建不再集成 Firebase 统计和性能服务。 + +## 3. 隔离签名并加固验证工作流 + +- [x] 3.1 删除 `.github/workflows/legado.jks` 以及 `Test Build` 中复制签名文件、硬编码别名和密码的步骤。 +- [x] 3.2 将 `Test Build` 收敛为执行 `:app:testAppDebugUnitTest` 和 `:app:assembleAppDebug` 的验证 job,并只上传明确标注为 Debug 的 GitHub Actions artifact。 +- [x] 3.3 删除 `Test Build` 中上游专用的预发布、蓝奏云、测试分支、Telegram、Release 变体和映射文件分发逻辑。 +- [x] 3.4 删除 Android 测试与构建步骤的 `continue-on-error`,确保任一验证命令失败都会使 job 失败。 +- [x] 3.5 保留手动 `Release Build` 的 Secrets-only 签名校验与两个正式变体,仅删除 Firebase 文件变换,并确认不存在公开签名回退。 + +## 4. 验证与交付 + +- [x] 4.1 使用 `git ls-files` 和聚焦文本搜索确认当前跟踪树不再包含继承的 Firebase 配置、旧签名文件、硬编码签名凭据或失效引用。 +- [x] 4.2 运行 `./gradlew :app:testAppDebugUnitTest` 和 `./gradlew :app:assembleAppDebug`,记录实际结果;不把未运行的设备测试描述为通过。 +- [x] 4.3 检查 GitHub Actions YAML 语法及工作流 job、依赖和条件,确认 Debug artifact 仅在验证成功后上传,正式发布仍在签名 Secrets 缺失时失败。 +- [x] 4.4 运行 `openspec validate --all --strict`、`git diff --check`、`git diff --stat`,确认工件与实现一致且没有无关改动。 +- [ ] 4.5 通过 Pull Request 合并后,按“上游 Firebase 客户端配置已从当前分支移除且本仓库不再使用”的事实说明关闭历史 Secret scanning 告警;不声称上游 Key 已被撤销。