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/fdroid-build.yml b/.github/workflows/fdroid-build.yml index 0ac056a..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: @@ -25,9 +28,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 +49,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 diff --git a/.github/workflows/test-keystore-clean.yml b/.github/workflows/test-keystore-clean.yml new file mode 100644 index 0000000..caee7da --- /dev/null +++ b/.github/workflows/test-keystore-clean.yml @@ -0,0 +1,136 @@ +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 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-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 "✅ Standard release APK generated successfully" + else + echo "❌ Standard release APK not found" + exit 1 + fi + else + echo "❌ Android standard release build failed" + echo "Build output:" + 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 + + - 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/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_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..62fca33 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 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 = System.getenv("KEYSTORE_PASSWORD") ?: project.findProperty("KEYSTORE_PASSWORD") as String? } } @@ -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..fad65cb 100644 --- a/keystore/README.md +++ b/keystore/README.md @@ -1,18 +1,50 @@ -# 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 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, 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. + +### Secrets Configuration + +The following GitHub Actions secrets are required: +- `KEYSTORE_BASE64`: Base64-encoded production keystore file +- `KEYSTORE_PASSWORD`: Password for accessing the keystore (used for both store and key access) +- `KEY_ALIAS`: Alias of the signing key within the keystore + +### 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 - https://source.android.com/docs/security/features/apksigning \ No newline at end of file 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.