038d3e7d7bc7

fix: Require protected release signing injection (PB-26-025, v2.14.90)

time
2026-09-02 11:14 UTC · 1 month ago
machine
koh2
parent
c944bc468b35
4 files .gitignore+3 −0VERSION+1 −1app/build.gradle+31 −11scripts/kepr-build-apk.sh+15 −27
.gitignore+3 −0
@@ -0,0 +1,3 @@1+# Local SDK paths and developer-only configuration must never enter another VCS.2+local.properties3+

view file

VERSION+1 −1
@@ -1 +1 @@1-2.14.891+2.14.90

view file

app/build.gradle+31 −11
@@ -50,18 +50,25 @@5050 5151     signingConfigs {5252         release {53-            // Read from local.properties (koh-ignored) so the keystore and its54-            // password never enter version history. Absent on a machine without55-            // the key -> release build stays unsigned rather than failing.56-            def props = new Properties()57-            def f = rootProject.file("local.properties")58-            if (f.exists()) { f.withInputStream { props.load(it) } }59-            if (props.getProperty("peachStoreFile")) {60-                storeFile file(props.getProperty("peachStoreFile"))61-                storePassword props.getProperty("peachStorePassword")62-                keyAlias props.getProperty("peachKeyAlias")63-                keyPassword props.getProperty("peachKeyPassword")53+            // Release authority comes only from process-scoped protected54+            // injection. local.properties is limited to non-secret local SDK55+            // configuration and is never a signing-secret store.56+            def signingEnvironment = [57+                storeFile: System.getenv("PEACH_RELEASE_STORE_FILE"),58+                storePassword: System.getenv("PEACH_RELEASE_STORE_PASSWORD"),59+                keyAlias: System.getenv("PEACH_RELEASE_KEY_ALIAS"),60+                keyPassword: System.getenv("PEACH_RELEASE_KEY_PASSWORD"),61+            ]62+            def configuredValues = signingEnvironment.values().count { it != null && !it.isEmpty() }63+            if (configuredValues != 0 && configuredValues != signingEnvironment.size()) {64+                throw new GradleException("incomplete protected release signing environment")6465             }66+            if (configuredValues == signingEnvironment.size()) {67+                storeFile file(signingEnvironment.storeFile)68+                storePassword signingEnvironment.storePassword69+                keyAlias signingEnvironment.keyAlias70+                keyPassword signingEnvironment.keyPassword71+            }6572             // v3 is the only escape hatch from "this key is forever": it carries a6673             // rotation lineage, so a future key can be proven to descend from this6774             // one instead of forcing every tester to uninstall and lose their@@ -90,6 +97,19 @@9097             signingConfig signingConfigs.release9198             minifyEnabled false9299             proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"100+        }101+    }102+103+    // Never let a release task silently emit an unsigned artifact. Debug tasks104+    // remain usable in fresh workspaces that do not hold release authority.105+    gradle.taskGraph.whenReady { graph ->106+        if (graph.allTasks.any { it.project == project && it.name.toLowerCase().contains("release") } &&107+            !signingConfigs.release.isSigningReady()) {108+            throw new GradleException(109+                "release signing requires PEACH_RELEASE_STORE_FILE, " +110+                    "PEACH_RELEASE_STORE_PASSWORD, PEACH_RELEASE_KEY_ALIAS, and " +111+                    "PEACH_RELEASE_KEY_PASSWORD",112+            )93113         }94114     }95115 

view file

scripts/kepr-build-apk.sh+15 −27
@@ -1,8 +1,8 @@11 #!/bin/sh22 # Build and publish the signed release APK for Kepr Builds.33 #4-# The release keystore is read from local.properties (koh-ignored). The script5-# checks that configuration before starting Gradle, then validates the produced4+# Release authority is injected through protected process environment variables.5+# The script checks that configuration before starting Gradle, then validates the produced66 # APK's package, version, APK Signature Scheme, and pinned release certificate77 # before copying anything to the Kepr staging directory.88 set -eu@@ -22,44 +22,32 @@2222 # shellcheck source=scripts/kepr-build-helpers.sh2323 . "$ROOT_DIR/scripts/kepr-build-helpers.sh"2424 25-PROPERTIES_FILE="$ROOT_DIR/local.properties"26-if [ ! -r "$PROPERTIES_FILE" ]; then27-    echo "refusing to publish: local.properties is missing or unreadable" >&228-    exit 129-fi25+: "${PEACH_RELEASE_STORE_FILE:=}"26+: "${PEACH_RELEASE_STORE_PASSWORD:=}"27+: "${PEACH_RELEASE_KEY_ALIAS:=}"28+: "${PEACH_RELEASE_KEY_PASSWORD:=}"3029 31-property() {32-    # Values are inspected only for presence/path checks; never print this file33-    # because it contains the release passwords.34-    sed -n "s/^$1=//p" "$PROPERTIES_FILE" | sed -n '1p'35-}36-37-STORE_FILE_PROPERTY=$(property peachStoreFile)38-STORE_PASSWORD=$(property peachStorePassword)39-KEY_ALIAS=$(property peachKeyAlias)40-KEY_PASSWORD=$(property peachKeyPassword)41-42-if [ -z "$STORE_FILE_PROPERTY" ] || [ -z "$STORE_PASSWORD" ] ||43-    [ -z "$KEY_ALIAS" ] || [ -z "$KEY_PASSWORD" ]; then44-    echo "refusing to publish: incomplete release signing configuration" >&230+if [ -z "$PEACH_RELEASE_STORE_FILE" ] || [ -z "$PEACH_RELEASE_STORE_PASSWORD" ] ||31+    [ -z "$PEACH_RELEASE_KEY_ALIAS" ] || [ -z "$PEACH_RELEASE_KEY_PASSWORD" ]; then32+    echo "refusing to publish: protected release signing environment is incomplete" >&24533     exit 14634 fi4735 48-case "$STORE_FILE_PROPERTY" in49-    /*) STORE_FILE="$STORE_FILE_PROPERTY" ;;50-    *) STORE_FILE="$ROOT_DIR/$STORE_FILE_PROPERTY" ;;36+case "$PEACH_RELEASE_STORE_FILE" in37+    /*) STORE_FILE="$PEACH_RELEASE_STORE_FILE" ;;38+    *) STORE_FILE="$ROOT_DIR/$PEACH_RELEASE_STORE_FILE" ;;5139 esac5240 if [ ! -f "$STORE_FILE" ] || [ ! -r "$STORE_FILE" ]; then5341     echo "refusing to publish: configured release keystore is missing or unreadable" >&25442     exit 15543 fi5644 57-# Resolve the SDK from the environment first, then the same local.properties58-# Gradle uses. Pick the newest *versioned* build-tools directory containing both45+# Resolve the SDK from the environment first, then the non-secret sdk.dir in46+# local.properties. Pick the newest *versioned* build-tools directory containing both5947 # tools; build-tools/latest is intentionally never trusted or consulted.6048 SDK_ROOT="${ANDROID_HOME:-${ANDROID_SDK_ROOT:-}}"6149 if [ -z "$SDK_ROOT" ]; then62-    SDK_ROOT=$(property sdk.dir)50+    SDK_ROOT=$(sed -n 's/^sdk\.dir=//p' "$ROOT_DIR/local.properties" 2>/dev/null | sed -n '1p')6351 fi6452 if [ -z "$SDK_ROOT" ] || [ ! -d "$SDK_ROOT/build-tools" ]; then6553     echo "refusing to publish: Android SDK build-tools directory not found" >&2

view file