#!/bin/sh # Build and publish the signed release APK for Kepr Builds. # # The release keystore is read from local.properties (koh-ignored). The script # checks that configuration before starting Gradle, then validates the produced # APK's package, version, APK Signature Scheme, and pinned release certificate # before copying anything to the Kepr staging directory. set -eu JAVA_HOME="${JAVA_HOME:-/Applications/Android Studio.app/Contents/jbr/Contents/Home}" export JAVA_HOME ROOT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) cd "$ROOT_DIR" # Keep the tool-selection and public metadata gates shared with the hermetic # negative-path test; this script owns signing/build orchestration only. # shellcheck source=scripts/kepr-build-helpers.sh . "$ROOT_DIR/scripts/kepr-build-helpers.sh" PROPERTIES_FILE="$ROOT_DIR/local.properties" if [ ! -r "$PROPERTIES_FILE" ]; then echo "refusing to publish: local.properties is missing or unreadable" >&2 exit 1 fi property() { # Values are inspected only for presence/path checks; never print this file # because it contains the release passwords. sed -n "s/^$1=//p" "$PROPERTIES_FILE" | sed -n '1p' } STORE_FILE_PROPERTY=$(property peachStoreFile) STORE_PASSWORD=$(property peachStorePassword) KEY_ALIAS=$(property peachKeyAlias) KEY_PASSWORD=$(property peachKeyPassword) if [ -z "$STORE_FILE_PROPERTY" ] || [ -z "$STORE_PASSWORD" ] || [ -z "$KEY_ALIAS" ] || [ -z "$KEY_PASSWORD" ]; then echo "refusing to publish: incomplete release signing configuration" >&2 exit 1 fi case "$STORE_FILE_PROPERTY" in /*) STORE_FILE="$STORE_FILE_PROPERTY" ;; *) STORE_FILE="$ROOT_DIR/$STORE_FILE_PROPERTY" ;; esac if [ ! -f "$STORE_FILE" ] || [ ! -r "$STORE_FILE" ]; then echo "refusing to publish: configured release keystore is missing or unreadable" >&2 exit 1 fi # Resolve the SDK from the environment first, then the same local.properties # Gradle uses. Pick the newest *versioned* build-tools directory containing both # tools; build-tools/latest is intentionally never trusted or consulted. SDK_ROOT="${ANDROID_HOME:-${ANDROID_SDK_ROOT:-}}" if [ -z "$SDK_ROOT" ]; then SDK_ROOT=$(property sdk.dir) fi if [ -z "$SDK_ROOT" ] || [ ! -d "$SDK_ROOT/build-tools" ]; then echo "refusing to publish: Android SDK build-tools directory not found" >&2 exit 1 fi BUILD_TOOLS_DIR=$(find_stable_build_tools_dir "$SDK_ROOT" || true) if [ -z "$BUILD_TOOLS_DIR" ]; then echo "refusing to publish: no versioned build-tools directory has apksigner and aapt2" >&2 exit 1 fi APKSIGNER="$BUILD_TOOLS_DIR/apksigner" AAPT2="$BUILD_TOOLS_DIR/aapt2" ROOT_VERSION=$(read_root_semver "$ROOT_DIR/VERSION" || true) if [ -z "$ROOT_VERSION" ]; then echo "refusing to publish: VERSION is missing or not strict semantic version" >&2 exit 1 fi "$ROOT_DIR/gradlew" --no-configuration-cache :app:assembleRelease case "${KEPR_TARGET:-}" in android-arm64) APK_NAME="app-arm64-v8a-release.apk" EXPECTED_ABI="arm64-v8a" ;; android-x86) APK_NAME="app-x86_64-release.apk" EXPECTED_ABI="x86_64" ;; *) echo "refusing to publish: unsupported KEPR_TARGET '${KEPR_TARGET:-unset}'" >&2 exit 1 ;; esac APK="$ROOT_DIR/app/build/outputs/apk/release/$APK_NAME" METADATA="$ROOT_DIR/app/build/outputs/apk/release/output-metadata.json" if [ ! -f "$APK" ] || [ ! -r "$APK" ]; then echo "refusing to publish: release APK was not produced" >&2 exit 1 fi if [ ! -f "$METADATA" ] || [ ! -r "$METADATA" ]; then echo "refusing to publish: Gradle output metadata was not produced" >&2 exit 1 fi BADGING_FILE=$(mktemp "${TMPDIR:-/tmp}/peach-apk-badging.XXXXXX") cleanup_badging() { [ -e "$BADGING_FILE" ] && trash "$BADGING_FILE" } trap cleanup_badging EXIT HUP INT TERM "$AAPT2" dump badging "$APK" > "$BADGING_FILE" if ! validate_release_metadata \ "$METADATA" \ "$BADGING_FILE" \ "$ROOT_VERSION" \ "com.peachpasswords" \ "$APK_NAME" \ "$EXPECTED_ABI"; then exit 1 fi # apksigner's plain verify accepts v1-only APKs, so require an explicit v2/v3/v4 # success line before publication. Capture output in memory; it contains only # public certificate metadata and is never written with signing secrets. SIGNING_OUTPUT=$("$APKSIGNER" verify --verbose --print-certs "$APK" 2>/dev/null) || { echo "refusing to publish: apksigner verification failed" >&2 exit 1 } if ! printf '%s\n' "$SIGNING_OUTPUT" | grep -Eq 'Verified using v[234] scheme.*: true'; then echo "refusing to publish: APK is not signed with APK Signature Scheme v2 or newer" >&2 exit 1 fi PINNED_RELEASE_CERT_SHA256="e10b975178f0bee7fb5a4802519da70ec2988b6ef7d8c9401fbbcff5a1137668" CERT_DIGEST_COUNT=$(printf '%s\n' "$SIGNING_OUTPUT" | grep -c 'certificate SHA-256 digest:' || true) if [ "$CERT_DIGEST_COUNT" -ne 1 ]; then echo "refusing to publish: release APK must have exactly one signer certificate" >&2 exit 1 fi CERT_DIGEST=$(printf '%s\n' "$SIGNING_OUTPUT" | sed -n 's/.*certificate SHA-256 digest:[[:space:]]*//p' | sed -n '1p' | tr -d '[:space:]' | tr '[:upper:]' '[:lower:]') if [ "$CERT_DIGEST" != "$PINNED_RELEASE_CERT_SHA256" ]; then echo "refusing to publish: APK signer certificate does not match the pinned Peach release digest" >&2 exit 1 fi # Only a fully verified artifact reaches the Kepr staging directory. mkdir -p "$ROOT_DIR/build/kepr" cp "$APK" "$ROOT_DIR/build/kepr/peach-android.apk" echo "release APK verified and staged: build/kepr/peach-android.apk"