From edc6efc600583a590bc0cc9b344ee07c4150a5ea Mon Sep 17 00:00:00 2001 From: Hossain Khan Date: Sat, 28 Jun 2025 12:47:01 -0400 Subject: [PATCH 1/4] [UPDATE] Enable android app release signing from CI --- .github/workflows/android-release.yml | 7 ++ .github/workflows/test-keystore-clean.yml | 114 ++++++++++++++++++++++ KEYSTORE_RESOLUTION.md | 0 app/build.gradle.kts | 23 +++-- keystore/README.md | 38 ++++++-- 5 files changed, 164 insertions(+), 18 deletions(-) create mode 100644 .github/workflows/test-keystore-clean.yml create mode 100644 KEYSTORE_RESOLUTION.md diff --git a/.github/workflows/android-release.yml b/.github/workflows/android-release.yml index 2fc439b..23e3661 100644 --- a/.github/workflows/android-release.yml +++ b/.github/workflows/android-release.yml @@ -33,8 +33,15 @@ jobs: - name: Setup Gradle uses: gradle/actions/setup-gradle@v4 + - name: Decode keystore from base64 + run: | + echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 -d > keystore/trmnl-app-release.keystore + - name: Build Release APK run: ./gradlew assembleStandardRelease + env: + KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }} + KEY_ALIAS: ${{ secrets.KEY_ALIAS }} - name: Extract version name id: version diff --git a/.github/workflows/test-keystore-clean.yml b/.github/workflows/test-keystore-clean.yml new file mode 100644 index 0000000..3e05f2a --- /dev/null +++ b/.github/workflows/test-keystore-clean.yml @@ -0,0 +1,114 @@ +name: Test Keystore Configuration + +# This workflow tests the production keystore configuration to ensure it works properly +# with the Android build system. It validates that the keystore can be decoded, passwords +# are correct, and jarsigner can successfully sign APKs. + +on: + workflow_dispatch: + +jobs: + test-keystore: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK + uses: actions/setup-java@v4 + with: + java-version: '23' + distribution: 'temurin' + + - name: Create test directory + run: mkdir -p keystore-test + + - name: Decode keystore from base64 + run: | + echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 -d > keystore-test/release.keystore + + - name: Verify keystore basic properties + run: | + echo "=== Keystore Basic Information ===" + keytool -list -keystore keystore-test/release.keystore -storepass "${{ secrets.KEYSTORE_PASSWORD }}" | grep -v "Certificate fingerprint" + + - name: Check alias exists + run: | + echo "=== Checking Alias ===" + if keytool -list -keystore keystore-test/release.keystore -storepass "${{ secrets.KEYSTORE_PASSWORD }}" -alias "${{ secrets.KEY_ALIAS }}" > /dev/null 2>&1; then + echo "✅ Alias '${{ secrets.KEY_ALIAS }}' exists in keystore" + else + echo "❌ Alias '${{ secrets.KEY_ALIAS }}' not found in keystore" + exit 1 + fi + + - name: Test jarsigner functionality + run: | + echo "=== Testing jarsigner ===" + + # Create a simple JAR to test signing + echo "Test content" > keystore-test/test.txt + jar cf keystore-test/test.jar keystore-test/test.txt + + # Test signing WITHOUT -keypass (the working approach) + echo "Testing jarsigner without explicit key password..." + if jarsigner -keystore keystore-test/release.keystore \ + -storepass "${{ secrets.KEYSTORE_PASSWORD }}" \ + keystore-test/test.jar \ + "${{ secrets.KEY_ALIAS }}" > keystore-test/jarsigner.txt 2>&1; then + echo "✅ jarsigner succeeded (store password used for both store and key)" + echo "✅ This confirms the keystore is compatible with our build configuration" + else + echo "❌ jarsigner failed even without explicit key password" + cat keystore-test/jarsigner.txt + exit 1 + fi + + # Verify the signed JAR + if jarsigner -verify keystore-test/test.jar > keystore-test/verify.txt 2>&1; then + echo "✅ Signed JAR verification successful" + else + echo "❌ Signed JAR verification failed" + cat keystore-test/verify.txt + exit 1 + fi + + - name: Test Android build configuration + run: | + echo "=== Testing Android Build Configuration ===" + + # Decode keystore to the expected location + echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 -d > keystore/trmnl-app-release.keystore + + # Set up Gradle + chmod +x gradlew + + # Test the release build (this will use the new signing configuration) + echo "Building release APK with production keystore..." + if ./gradlew assembleStandardRelease \ + -PKEYSTORE_PASSWORD="${{ secrets.KEYSTORE_PASSWORD }}" \ + -PKEY_ALIAS="${{ secrets.KEY_ALIAS }}" > keystore-test/gradle-build.txt 2>&1; then + echo "✅ Android release build succeeded with production keystore" + echo "✅ APK should be properly signed" + + # Verify the APK exists + if [ -f "app/build/outputs/apk/standard/release/app-standard-release.apk" ]; then + echo "✅ Release APK generated successfully" + else + echo "❌ Release APK not found" + exit 1 + fi + else + echo "❌ Android release build failed" + echo "Build output:" + cat keystore-test/gradle-build.txt + exit 1 + fi + + - name: Upload test artifacts + if: always() + uses: actions/upload-artifact@v4 + with: + name: keystore-test-results + path: keystore-test/ + retention-days: 7 diff --git a/KEYSTORE_RESOLUTION.md b/KEYSTORE_RESOLUTION.md new file mode 100644 index 0000000..e69de29 diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2ddf48c..1088e79 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -42,16 +42,16 @@ android { } create("release") { - // Uses the same debug keystore for release builds to enable CLI building - // ⚠️ This is temporary solution as app is still under development - // It allows early adopters to test drive the app without us worrying about signing key. - // - 📚 https://github.com/usetrmnl/trmnl-android/blob/main/keystore/README.md - storeFile = file("${rootProject.projectDir}/keystore/debug.keystore") + // Production keystore for release builds + // The keystore file should be decoded from KEYSTORE_BASE64 secret in CI/CD + storeFile = file("${rootProject.projectDir}/keystore/trmnl-app-release.keystore") - // ℹ️ When time comes, these values should come from `secret.properties` file or CI/CD secrets. - storePassword = "android" - keyAlias = "androiddebugkey" - keyPassword = "android" + // Values come from CI/CD secrets or local secret.properties file + // Note: keyPassword is intentionally omitted - this keystore works when + // jarsigner uses the store password for both store and key access + storePassword = System.getenv("KEYSTORE_PASSWORD") ?: project.findProperty("KEYSTORE_PASSWORD") as String? + keyAlias = System.getenv("KEY_ALIAS") ?: project.findProperty("KEY_ALIAS") as String? + // keyPassword = ... // Intentionally omitted - see note above } } @@ -99,7 +99,10 @@ android { // F-Droid specific configuration // No non-free dependencies buildConfigField("Boolean", "FDROID_BUILD", "true") - // ℹ️ No signing config for F-Droid flavor (F-Droid handles signing) + + // ℹ️ Also sign the F-Droid build with the release keystore + // F-Droid will use reproducible builds process to validate authenticity + signingConfig = signingConfigs.getByName("release") } } diff --git a/keystore/README.md b/keystore/README.md index c1170fe..96b26a3 100644 --- a/keystore/README.md +++ b/keystore/README.md @@ -1,18 +1,40 @@ -# Debug Keystore +# App Signing Keystores -The debug keystore file is added to the repository to make it easier for developers to build and run +This directory contains the keystores used for signing the Android app. + +## Debug Keystore + +The debug keystore file (`debug.keystore`) is added to the repository to make it easier for developers to build and run the app without having to generate a new keystore file each time. The debug keystore is used for -signing the app during development and [CI builds](https://github.com/usetrmnl/trmnl-android/actions/workflows/android-release.yml), -which allows early adopters test drive the app. - -This solution is **not intended for production use**, and can't be used to publish the app to the -Google Play Store. If that time ever comes, aside from creating release keystore, there needs to be -a clear strategy for release and maintenance of the keystore file with passcodes. +signing the app during development. > [!NOTE] > The debug keystore is generated automatically by Android Studio > and copied from the `$HOME/.android/debug.keystore` location. +## Production Keystore + +The production keystore (`trmnl-app-release.keystore`) is used for release builds and is stored as a base64-encoded secret in GitHub Actions. The keystore is decoded during CI/CD builds. + +### Important Notes About the Production Keystore + +The production keystore has a specific configuration quirk that's important to understand: + +- **Store Password**: Used to access the keystore file +- **Key Password**: The keystore was created with a key password, but due to PKCS12 format behavior, the private key is only accessible when jarsigner uses the store password for both store and key access +- **Solution**: The `keyPassword` parameter is intentionally omitted from the Android build configuration, allowing the Android build system to use the store password for both purposes + +This is a known characteristic of certain PKCS12 keystores where explicit key passwords can cause "key associated with alias not a private key" errors, even when the keystore is completely valid. + +### Secrets Configuration + +The following GitHub Actions secrets are required: +- `KEYSTORE_BASE64`: Base64-encoded production keystore file +- `KEYSTORE_PASSWORD`: Password for accessing the keystore +- `KEY_ALIAS`: Alias of the signing key within the keystore + +Note that `KEY_PASSWORD` is not used in the build configuration due to the keystore behavior described above. + ## Related Resources - https://developer.android.com/studio/publish/app-signing - https://source.android.com/docs/security/features/apksigning \ No newline at end of file From a6c908996819c216e40ce73b3013434cc66433fc Mon Sep 17 00:00:00 2001 From: Hossain Khan Date: Sat, 28 Jun 2025 12:53:16 -0400 Subject: [PATCH 2/4] [FIXED] F-droid build workflow. --- .github/workflows/fdroid-build.yml | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/.github/workflows/fdroid-build.yml b/.github/workflows/fdroid-build.yml index 0ac056a..c7ba3d4 100644 --- a/.github/workflows/fdroid-build.yml +++ b/.github/workflows/fdroid-build.yml @@ -25,9 +25,16 @@ jobs: - name: Setup Gradle uses: gradle/actions/setup-gradle@v4 + + - name: Decode keystore from base64 + run: | + echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 -d > keystore/trmnl-app-release.keystore - name: Build F-Droid APK run: ./gradlew buildFDroid + env: + KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }} + KEY_ALIAS: ${{ secrets.KEY_ALIAS }} - name: Extract version name id: version @@ -39,13 +46,13 @@ jobs: - name: Rename APK run: | mkdir -p artifact - cp app/build/outputs/apk/fdroid/release/app-fdroid-release-unsigned.apk artifact/trmnl-mirror-fdroid-v${{ steps.version.outputs.VERSION }}-unsigned.apk + cp app/build/outputs/apk/fdroid/release/app-fdroid-release.apk artifact/trmnl-mirror-fdroid-v${{ steps.version.outputs.VERSION }}.apk - name: Upload F-Droid APK uses: actions/upload-artifact@v4 with: - name: trmnl-mirror-fdroid-unsigned - path: artifact/trmnl-mirror-fdroid-v${{ steps.version.outputs.VERSION }}-unsigned.apk + name: trmnl-mirror-fdroid-signed + path: artifact/trmnl-mirror-fdroid-v${{ steps.version.outputs.VERSION }}.apk # Use maximum allowed retention period # https://github.com/actions/upload-artifact?tab=readme-ov-file#retention-period retention-days: 90 From d8f91d8e6a52865ec3f2dae55c9630758cc9439e Mon Sep 17 00:00:00 2001 From: Hossain Khan Date: Sat, 28 Jun 2025 12:58:55 -0400 Subject: [PATCH 3/4] [FIXED] pass missing > Task :app:packageFdroidRelease FAILED gradle/actions: Writing build results to /home/runner/work/_temp/.gradle-actions/build-results/__run_2-1751129646875.json [Incubating] Problems report is available at: file:///home/runner/work/trmnl-android/trmnl-android/build/reports/problems/problems-report.html FAILURE: Build failed with an exception. * What went wrong: Execution failed for task ':app:packageFdroidRelease'. > A failure occurred while executing com.android.build.gradle.tasks.PackageAndroidArtifact$IncrementalSplitterRunnable > SigningConfig "release" is missing required property "keyPassword". --- app/build.gradle.kts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 1088e79..62fca33 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -47,11 +47,11 @@ android { storeFile = file("${rootProject.projectDir}/keystore/trmnl-app-release.keystore") // Values come from CI/CD secrets or local secret.properties file - // Note: keyPassword is intentionally omitted - this keystore works when - // jarsigner uses the store password for both store and key access + // Note: keyPassword is set to the same value as storePassword because this PKCS12 keystore + // requires the store password to be used for both store and key access storePassword = System.getenv("KEYSTORE_PASSWORD") ?: project.findProperty("KEYSTORE_PASSWORD") as String? keyAlias = System.getenv("KEY_ALIAS") ?: project.findProperty("KEY_ALIAS") as String? - // keyPassword = ... // Intentionally omitted - see note above + keyPassword = System.getenv("KEYSTORE_PASSWORD") ?: project.findProperty("KEYSTORE_PASSWORD") as String? } } From 95757544646b98738bc500ca0d23307dcf6716a3 Mon Sep 17 00:00:00 2001 From: Hossain Khan Date: Sat, 28 Jun 2025 13:09:33 -0400 Subject: [PATCH 4/4] [UPDATE] Docs to mention f-droid is also now signed --- .github/workflows/fdroid-build.yml | 5 ++- .github/workflows/test-keystore-clean.yml | 40 ++++++++++++++++++----- BUILD_FDROID.md | 6 ++-- keystore/README.md | 20 +++++++++--- metadata/ink.trmnl.android.yml | 2 +- 5 files changed, 55 insertions(+), 18 deletions(-) diff --git a/.github/workflows/fdroid-build.yml b/.github/workflows/fdroid-build.yml index c7ba3d4..74a277d 100644 --- a/.github/workflows/fdroid-build.yml +++ b/.github/workflows/fdroid-build.yml @@ -1,11 +1,14 @@ name: F-Droid Build +# Builds a signed F-Droid APK using the production keystore. +# F-Droid will verify the signature during their reproducible build process. + on: push: branches: [ "main" ] pull_request: branches: [ "main" ] - # This allows manual triggering of the workflow, which results in making F-Droid APK build + # This allows manual triggering of the workflow, which results in making a signed F-Droid APK build # Go to the "Actions" tab the repository, select this workflow, and click the "Run workflow" button to run it manually. workflow_dispatch: diff --git a/.github/workflows/test-keystore-clean.yml b/.github/workflows/test-keystore-clean.yml index 3e05f2a..caee7da 100644 --- a/.github/workflows/test-keystore-clean.yml +++ b/.github/workflows/test-keystore-clean.yml @@ -83,25 +83,47 @@ jobs: # Set up Gradle chmod +x gradlew - # Test the release build (this will use the new signing configuration) - echo "Building release APK with production keystore..." + # Test the standard release build + echo "Building standard release APK with production keystore..." if ./gradlew assembleStandardRelease \ -PKEYSTORE_PASSWORD="${{ secrets.KEYSTORE_PASSWORD }}" \ - -PKEY_ALIAS="${{ secrets.KEY_ALIAS }}" > keystore-test/gradle-build.txt 2>&1; then - echo "✅ Android release build succeeded with production keystore" - echo "✅ APK should be properly signed" + -PKEY_ALIAS="${{ secrets.KEY_ALIAS }}" > keystore-test/gradle-build-standard.txt 2>&1; then + echo "✅ Android standard release build succeeded with production keystore" # Verify the APK exists if [ -f "app/build/outputs/apk/standard/release/app-standard-release.apk" ]; then - echo "✅ Release APK generated successfully" + echo "✅ Standard release APK generated successfully" else - echo "❌ Release APK not found" + echo "❌ Standard release APK not found" exit 1 fi else - echo "❌ Android release build failed" + echo "❌ Android standard release build failed" echo "Build output:" - cat keystore-test/gradle-build.txt + cat keystore-test/gradle-build-standard.txt + exit 1 + fi + + # Test the F-Droid release build + echo "" + echo "Building F-Droid release APK with production keystore..." + if ./gradlew assembleFdroidRelease \ + -PKEYSTORE_PASSWORD="${{ secrets.KEYSTORE_PASSWORD }}" \ + -PKEY_ALIAS="${{ secrets.KEY_ALIAS }}" > keystore-test/gradle-build-fdroid.txt 2>&1; then + echo "✅ Android F-Droid release build succeeded with production keystore" + + # Verify the APK exists + if [ -f "app/build/outputs/apk/fdroid/release/app-fdroid-release.apk" ]; then + echo "✅ F-Droid release APK generated successfully" + echo "✅ Both standard and F-Droid builds are now properly signed" + else + echo "❌ F-Droid release APK not found" + exit 1 + fi + else + echo "❌ Android F-Droid release build failed" + echo "Build output:" + cat keystore-test/gradle-build-fdroid.txt exit 1 fi diff --git a/BUILD_FDROID.md b/BUILD_FDROID.md index e43e87b..4eda97e 100644 --- a/BUILD_FDROID.md +++ b/BUILD_FDROID.md @@ -9,7 +9,7 @@ The app includes specific configurations for F-Droid compatibility: 1. A dedicated `fdroid` product flavor that excludes Google Fonts 2. A specific `fdroidRelease` build type 3. System fonts are used instead of Google Fonts for the F-Droid version -4. The F-Droid build is **not signed** (as per [PR #106](https://github.com/usetrmnl/trmnl-android/pull/106)) - F-Droid handles the signing process +4. The F-Droid build is **signed** with the production keystore for consistency across all distribution channels ## Building the F-Droid Version @@ -19,7 +19,9 @@ To build the F-Droid version locally: ./gradlew assembleFdroidRelease ``` -This will generate an unsigned APK in `app/build/outputs/apk/fdroid/release/` that is suitable for F-Droid submission. Unlike the standard release build, the F-Droid build variant does not have a signing configuration, as F-Droid's build system will handle the signing process. +This will generate a signed APK in `app/build/outputs/apk/fdroid/release/` using the production keystore. The F-Droid build variant now uses the same signing configuration as the standard release build, ensuring consistency across all distribution channels. + +> **Note**: F-Droid's reproducible build process will verify that the APK can be rebuilt with the same signature, ensuring the integrity of the build process. Alternatively, you can use the convenience task: diff --git a/keystore/README.md b/keystore/README.md index 96b26a3..fad65cb 100644 --- a/keystore/README.md +++ b/keystore/README.md @@ -14,15 +14,20 @@ signing the app during development. ## Production Keystore -The production keystore (`trmnl-app-release.keystore`) is used for release builds and is stored as a base64-encoded secret in GitHub Actions. The keystore is decoded during CI/CD builds. +The production keystore (`trmnl-app-release.keystore`) is used for all release builds (both standard and F-Droid flavors) and is stored as a base64-encoded secret in GitHub Actions. The keystore is decoded during CI/CD builds. + +### Build Flavors Using Production Keystore + +- **Standard Release**: Signs APKs for general distribution +- **F-Droid Release**: Signs APKs for F-Droid distribution (F-Droid will verify the signature during their reproducible build process) ### Important Notes About the Production Keystore The production keystore has a specific configuration quirk that's important to understand: - **Store Password**: Used to access the keystore file -- **Key Password**: The keystore was created with a key password, but due to PKCS12 format behavior, the private key is only accessible when jarsigner uses the store password for both store and key access -- **Solution**: The `keyPassword` parameter is intentionally omitted from the Android build configuration, allowing the Android build system to use the store password for both purposes +- **Key Password**: The keystore was created with a key password, but due to PKCS12 format behavior, both `storePassword` and `keyPassword` are set to the same value in the build configuration +- **Solution**: The Android Gradle Plugin requires both passwords to be explicitly set, so we use the store password for both purposes This is a known characteristic of certain PKCS12 keystores where explicit key passwords can cause "key associated with alias not a private key" errors, even when the keystore is completely valid. @@ -30,10 +35,15 @@ This is a known characteristic of certain PKCS12 keystores where explicit key pa The following GitHub Actions secrets are required: - `KEYSTORE_BASE64`: Base64-encoded production keystore file -- `KEYSTORE_PASSWORD`: Password for accessing the keystore +- `KEYSTORE_PASSWORD`: Password for accessing the keystore (used for both store and key access) - `KEY_ALIAS`: Alias of the signing key within the keystore -Note that `KEY_PASSWORD` is not used in the build configuration due to the keystore behavior described above. +### CI/CD Workflows Using Production Keystore + +- **`android-release.yml`**: Builds and signs standard release APKs +- **`fdroid-build.yml`**: Builds and signs F-Droid release APKs + +Both workflows decode the keystore from the base64 secret and provide the necessary environment variables for signing. ## Related Resources - https://developer.android.com/studio/publish/app-signing diff --git a/metadata/ink.trmnl.android.yml b/metadata/ink.trmnl.android.yml index c747759..be52225 100644 --- a/metadata/ink.trmnl.android.yml +++ b/metadata/ink.trmnl.android.yml @@ -41,7 +41,7 @@ Builds: subdir: app gradle: - fdroid - output: build/outputs/apk/fdroid/release/app-fdroid-release-unsigned.apk + output: build/outputs/apk/fdroid/release/app-fdroid-release.apk MaintainerNotes: This app uses the F-Droid flavor for gradle build.