From a6d930415c4bdc836767929c8b2815e1ccd01a1e Mon Sep 17 00:00:00 2001 From: Amir Khodak Date: Wed, 10 Jun 2026 10:28:37 +0000 Subject: [PATCH] Fix empty usage-history chart + externalize signing secrets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit History chart: recordHistory() threw away the previous point whenever a new reading landed within the 2-min de-dup window, but the foreground loop refreshes every 30s — so history could never grow past one point while the app was open and the chart stayed stuck on 'Collecting history…'. Now it throttles by SKIPPING a too-soon reading instead of replacing the last one, so points accumulate during normal use. Security: - Remove hardcoded release keystore passwords from build.gradle.kts; read from env vars / gitignored keystore.properties; CI injects from Gitea secrets (KEYSTORE_PASSWORD/KEY_PASSWORD). Signing identity unchanged. - Make the cookie-never-plaintext invariant explicit on the read path. - Drop custom ACTION_REFRESH from the exported widget intent-filter so other apps can't trigger refreshes; internal explicit PendingIntent still works. - Gate an unguarded Log.w behind BuildConfig.DEBUG. --- .gitea/workflows/build.yml | 14 +++++++ .gitignore | 1 + app/build.gradle.kts | 39 ++++++++++++++++--- app/src/main/AndroidManifest.xml | 4 +- .../me/khodak/claudeusage/UsageRepository.kt | 2 +- .../claudeusage/data/PreferencesManager.kt | 19 ++++++--- keystore.properties.example | 12 ++++++ 7 files changed, 78 insertions(+), 13 deletions(-) create mode 100644 keystore.properties.example diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml index 698ba27..2eed104 100644 --- a/.gitea/workflows/build.yml +++ b/.gitea/workflows/build.yml @@ -56,6 +56,20 @@ jobs: fi echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 -d > app/claude-widget-release.keystore + # Signing passwords are no longer hardcoded in build.gradle.kts — inject them at build time. + - name: Write signing credentials + if: startsWith(github.ref, 'refs/tags/') + run: | + if [ -z "${{ secrets.KEYSTORE_PASSWORD }}" ] || [ -z "${{ secrets.KEY_PASSWORD }}" ]; then + echo "::error::KEYSTORE_PASSWORD / KEY_PASSWORD secrets not set — cannot sign release." + exit 1 + fi + { + echo "storePassword=${{ secrets.KEYSTORE_PASSWORD }}" + echo "keyPassword=${{ secrets.KEY_PASSWORD }}" + echo "keyAlias=${{ secrets.KEY_ALIAS || 'claudewidget' }}" + } > keystore.properties + - name: Build release APK if: startsWith(github.ref, 'refs/tags/') run: ./gradlew :app:assembleRelease --no-daemon diff --git a/.gitignore b/.gitignore index e22edec..a966eb9 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ captures/ .cxx/ *.keystore *.jks +keystore.properties diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 090e73f..3c9347f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,8 +1,21 @@ +import java.util.Properties + plugins { id("com.android.application") id("org.jetbrains.kotlin.android") } +// Signing credentials are NEVER committed. They are read from (in order): +// 1. environment variables (KEYSTORE_PASSWORD / KEY_PASSWORD / KEY_ALIAS) — used by CI +// 2. keystore.properties at the repo root (gitignored) — used locally +// See keystore.properties.example. Debug builds need none of this. +val keystoreProps = Properties().apply { + val f = rootProject.file("keystore.properties") + if (f.exists()) f.inputStream().use { load(it) } +} +fun signingCred(envName: String, propName: String): String? = + System.getenv(envName) ?: keystoreProps.getProperty(propName) + android { namespace = "me.khodak.claudeusage" compileSdk = 34 @@ -15,12 +28,22 @@ android { versionName = "1.18" } + val releaseStorePassword = signingCred("KEYSTORE_PASSWORD", "storePassword") + val releaseKeyPassword = signingCred("KEY_PASSWORD", "keyPassword") + val releaseKeyAlias = signingCred("KEY_ALIAS", "keyAlias") ?: "claudewidget" + val hasSigningCreds = releaseStorePassword != null && releaseKeyPassword != null + signingConfigs { create("release") { - storeFile = file("claude-widget-release.keystore") - storePassword = "ClaudeWidget2026!" - keyAlias = "claudewidget" - keyPassword = "ClaudeWidget2026!" + // Only wire the keystore when credentials are present, so debug builds and + // credential-less checkouts configure cleanly. Same keystore file + alias as before — + // signing identity is unchanged; only the password source moved out of version control. + if (hasSigningCreds) { + storeFile = file("claude-widget-release.keystore") + storePassword = releaseStorePassword + keyAlias = releaseKeyAlias + keyPassword = releaseKeyPassword + } } } @@ -29,7 +52,13 @@ android { isMinifyEnabled = true isShrinkResources = true proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") - signingConfig = signingConfigs.getByName("release") + // Sign only when creds were supplied; otherwise fail loudly at assembleRelease rather + // than silently shipping an unsigned APK. Tag builds in CI inject creds (see workflow). + if (hasSigningCreds) { + signingConfig = signingConfigs.getByName("release") + } else { + logger.warn("No signing credentials (KEYSTORE_PASSWORD/keystore.properties) — release APK will be unsigned.") + } } } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index cee668d..26baa30 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -32,9 +32,11 @@ + -