Compare commits

...

4 Commits

Author SHA1 Message Date
Joe Julian fea1a75cdf Keep release signing secrets out of APK build logs 2026-04-18 22:16:25 -07:00
Joe Julian 0dfaeef7bf Require dedicated release signing for APK builds 2026-04-18 22:00:56 -07:00
Joe Julian 92a7853258 Harden Android shared-vault import intents 2026-04-16 21:33:40 -07:00
Joe Julian 14f22b4ebf Fix Android packaging asset discovery 2026-04-16 21:08:31 -07:00
10 changed files with 146 additions and 15 deletions
+9 -3
View File
@@ -45,8 +45,8 @@ Use this skill together with the installed `android-emulator-debug` skill. That
## Build Workflow
1. Verify the JDK/SDK paths match the known working environment.
2. Build with `make apk`.
3. If `make apk` fails, inspect the effective `JAVA_HOME`, `ANDROID_SDK_ROOT`, and `ANDROID_NDK_ROOT` before changing code.
2. Build with `make apk` for debug validation, or `make apk-release` when validating production signing behavior.
3. If the build fails, inspect the effective `JAVA_HOME`, `ANDROID_SDK_ROOT`, and `ANDROID_NDK_ROOT` before changing code.
4. If the problem is Android-only, avoid desktop-only conclusions from `go test ./...`.
Typical local build:
@@ -55,6 +55,12 @@ Typical local build:
JAVA_HOME=/usr/lib/jvm/java-25-openjdk make apk
```
Typical local release build:
```sh
JAVA_HOME=/usr/lib/jvm/java-25-openjdk make apk-release
```
## Emulator Workflow
1. Reuse an existing emulator session if one is already running.
@@ -79,7 +85,7 @@ adb shell dumpsys window | rg 'mCurrentFocus|mFocusedApp'
## Validation Checklist
- APK builds successfully with `make apk`.
- APK builds successfully with the intended target: `make apk` for debug validation or `make apk-release` for release-signing validation.
- App launches to `org.julianfamily.keepassgo/org.gioui.GioActivity`.
- Screenshot shows the expected screen, not just a black frame.
- `logcat` shows no app crash or Android runtime fatal error.
+7 -1
View File
@@ -52,11 +52,17 @@ The installed package version must correspond to the committed source, not a dir
Use the repo's known-good local JDK unless the environment already proves otherwise:
```sh
JAVA_HOME=/usr/lib/jvm/java-25-openjdk make apk
JAVA_HOME=/usr/lib/jvm/java-25-openjdk make apk-release
```
If that JDK is unavailable on the current host, use the working replacement already established for the machine and say so in the closeout.
- `ship it` must use the dedicated release keystore flow, not Gio's implicit debug or temporary signing path.
- The default local release-signing paths are:
`~/.config/keepassgo/android-release.keystore`
`~/.config/keepassgo/android-release.pass`
- If those files are unavailable, stop and fix signing instead of shipping a differently signed APK.
### 4. Zip The APK
- Create the ZIP under the globally required temporary secret-safe directory.
+6 -3
View File
@@ -135,11 +135,14 @@ jobs:
shell: bash
run: |
set -euo pipefail
signkey_path="$(mktemp)"
trap 'rm -f -- "$signkey_path"' EXIT
mkdir -p build/ci-signing
signkey_path="$(pwd)/build/ci-signing/android-release.keystore"
signpass_path="$(pwd)/build/ci-signing/android-release.pass"
trap 'rm -f -- "$signkey_path" "$signpass_path"' EXIT
printf '%s' '${{ secrets.APK_SIGNKEY_B64 }}' | base64 -d > "$signkey_path"
printf '%s' '${{ secrets.APK_SIGNPASS }}' > "$signpass_path"
export APP_VERSION="$(git describe --tags --always --dirty)"
make apk SIGNKEY="$signkey_path" SIGNPASS='${{ secrets.APK_SIGNPASS }}'
make apk-release RELEASE_SIGNKEY="$signkey_path" RELEASE_SIGNPASS_FILE="$signpass_path"
cp build/keepassgo.apk "${DIST_DIR}/keepassgo.apk"
- name: Upload CI artifacts
+23
View File
@@ -6,11 +6,22 @@ Build the APK with:
make apk
```
Build the release-signed APK with:
```sh
make apk-release
```
`make apk` uses a local Java 25 install when `JAVA_HOME` points to one.
If the host does not have a working Java 25 install, it falls back to the
repo-managed Docker image in `packaging/docker/android-apk/`, which also builds
with Java 25.
`make apk` remains a developer build path and may use Gio's default debug or
ephemeral signing behavior if no explicit signing key is provided.
`make apk-release` is the production-signing path and fails unless a dedicated
release keystore and password file are present.
Environment:
- `ANDROID_SDK_ROOT` defaults to `/opt/android-sdk`.
@@ -23,6 +34,14 @@ Environment:
- `APK_VERSION` overrides the packaged app version.
- `ANDROID_MIN_SDK` overrides the minimum supported Android SDK.
- `ANDROID_TARGET_SDK` overrides the target Android SDK.
- `SIGNPASS_FILE` provides the signing password by file instead of a command-line argument.
- `RELEASE_SIGNKEY` overrides the release keystore path used by `make apk-release`.
- `RELEASE_SIGNPASS_FILE` overrides the password file path used by `make apk-release`.
Default release-signing paths:
- `~/.config/keepassgo/android-release.keystore`
- `~/.config/keepassgo/android-release.pass`
Installed machine prerequisites expected by this repo:
@@ -38,6 +57,10 @@ The repo tracks `gogio` as a Go tool, and the local build runs through:
go tool gogio -target android ./cmd/keepassgo ...
```
The release target wraps `make apk` and injects explicit signing credentials so
local release builds and CI use the same stable key without echoing the release
password in build logs.
The Android build uses the branded icon asset at:
- `internal/assets/keepassgo-icon.png`
+33 -2
View File
@@ -12,6 +12,9 @@ ANDROID_MIN_SDK ?= 28
ANDROID_TARGET_SDK ?= 35
SIGNKEY ?=
SIGNPASS ?=
SIGNPASS_FILE ?=
RELEASE_SIGNKEY ?= $(HOME)/.config/keepassgo/android-release.keystore
RELEASE_SIGNPASS_FILE ?= $(HOME)/.config/keepassgo/android-release.pass
ARCH_PKG_DIR ?= packaging/archlinux/keepassgo-git
ARCH_PKG_TMPL ?= $(ARCH_PKG_DIR)/PKGBUILD.tmpl
ARCH_PKGBUILD ?= $(ARCH_PKG_DIR)/PKGBUILD
@@ -26,7 +29,22 @@ ifneq ($(strip $(SIGNPASS)),)
GOGIO_SIGN_FLAGS += -signpass $(SIGNPASS)
endif
.PHONY: apk apk-local apk-container apk-container-image archlinux-pkgbuild browser-bridge browser-extension-validate
CONTAINER_SIGNKEY_MOUNT :=
CONTAINER_SIGNPASSFILE_MOUNT :=
CONTAINER_SIGN_ARGS :=
ifneq ($(strip $(SIGNKEY)),)
CONTAINER_SIGNKEY_MOUNT += -v "$(dir $(abspath $(SIGNKEY))):$(dir $(abspath $(SIGNKEY))):ro"
CONTAINER_SIGN_ARGS += SIGNKEY="$(abspath $(SIGNKEY))"
endif
ifneq ($(strip $(SIGNPASS)),)
CONTAINER_SIGN_ARGS += SIGNPASS="$(SIGNPASS)"
endif
ifneq ($(strip $(SIGNPASS_FILE)),)
CONTAINER_SIGNPASSFILE_MOUNT += -v "$(dir $(abspath $(SIGNPASS_FILE))):$(dir $(abspath $(SIGNPASS_FILE))):ro"
CONTAINER_SIGN_ARGS += SIGNPASS_FILE="$(abspath $(SIGNPASS_FILE))"
endif
.PHONY: apk apk-local apk-release apk-container apk-container-image archlinux-pkgbuild browser-bridge browser-extension-validate
apk:
@if [ -x "$(JAVA_HOME)/bin/java" ] && "$(JAVA_HOME)/bin/java" -version 2>&1 | grep -q 'version "25'; then \
$(MAKE) apk-local JAVA_HOME="$(JAVA_HOME)"; \
@@ -43,6 +61,12 @@ apk-local: android/keepassgo-android.jar
@test -d "$(ANDROID_SDK_ROOT)/platforms/android-$(ANDROID_TARGET_SDK)" || { echo "Android platform android-$(ANDROID_TARGET_SDK) is missing"; exit 1; }
@test -d "$(ANDROID_SDK_ROOT)/build-tools" || { echo "Android build-tools are missing"; exit 1; }
@mkdir -p "$(dir $(APK_OUT))"
@set -eu; \
if [ -n "$(SIGNPASS_FILE)" ]; then \
test -f "$(SIGNPASS_FILE)" || { echo "SIGNPASS_FILE does not exist: $(SIGNPASS_FILE)"; exit 1; }; \
export GOGIO_SIGNPASS="$$(tr -d '\r\n' < "$(SIGNPASS_FILE)")"; \
test -n "$$GOGIO_SIGNPASS" || { echo "SIGNPASS_FILE is empty: $(SIGNPASS_FILE)"; exit 1; }; \
fi; \
ANDROID_HOME="$(ANDROID_SDK_ROOT)" \
ANDROID_SDK_ROOT="$(ANDROID_SDK_ROOT)" \
ANDROID_NDK_ROOT="$(ANDROID_NDK_ROOT)" \
@@ -59,6 +83,11 @@ apk-local: android/keepassgo-android.jar
-icon internal/assets/keepassgo-icon.png \
./cmd/keepassgo
apk-release:
@test -f "$(RELEASE_SIGNKEY)" || { echo "Release signing key not found at $(RELEASE_SIGNKEY)"; exit 1; }
@test -f "$(RELEASE_SIGNPASS_FILE)" || { echo "Release signing password file not found at $(RELEASE_SIGNPASS_FILE)"; exit 1; }
@$(MAKE) apk SIGNKEY="$(abspath $(RELEASE_SIGNKEY))" SIGNPASS_FILE="$(abspath $(RELEASE_SIGNPASS_FILE))"
apk-container: apk-container-image
@command -v docker >/dev/null 2>&1 || { echo "docker is required for apk-container"; exit 1; }
@test -d "$(ANDROID_SDK_ROOT)" || { echo "ANDROID_SDK_ROOT must point to an Android SDK install"; exit 1; }
@@ -69,11 +98,13 @@ apk-container: apk-container-image
-w "$(CURDIR)" \
-v "$(ANDROID_SDK_ROOT):$(ANDROID_SDK_ROOT)" \
-v "$(ANDROID_NDK_ROOT):$(ANDROID_NDK_ROOT)" \
$(CONTAINER_SIGNKEY_MOUNT) \
$(CONTAINER_SIGNPASSFILE_MOUNT) \
-e ANDROID_SDK_ROOT="$(ANDROID_SDK_ROOT)" \
-e ANDROID_NDK_ROOT="$(ANDROID_NDK_ROOT)" \
-e JAVA_HOME=/opt/java/openjdk \
$(APK_BUILD_IMAGE) \
make apk-local JAVA_HOME=/opt/java/openjdk
make apk-local JAVA_HOME=/opt/java/openjdk $(CONTAINER_SIGN_ARGS)
apk-container-image:
@command -v docker >/dev/null 2>&1 || { echo "docker is required for apk-container-image"; exit 1; }
+11
View File
@@ -98,6 +98,17 @@ available, it falls back to the repo-managed Docker build image, which also
uses Java 25. You still need the Android SDK and NDK installed and configured
for real device or release packaging.
Release package:
```bash
make apk-release
```
`make apk-release` is the production-signing path. It requires a dedicated
release keystore at `~/.config/keepassgo/android-release.keystore` and a
password file at `~/.config/keepassgo/android-release.pass`, unless you
override `RELEASE_SIGNKEY` and `RELEASE_SIGNPASS_FILE`.
## Automation
Desktop automation is resolved through the secure gRPC API rather than synthetic auto-type.
+3
View File
@@ -45,6 +45,9 @@
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="application/octet-stream" />
<data android:mimeType="application/x-keepass2" />
<data android:mimeType="application/vnd.keepass" />
<data android:scheme="content" android:pathPattern=".*\\.kdbx" />
<data android:scheme="file" android:pathPattern=".*\\.kdbx" />
</intent-filter>
@@ -1,6 +1,7 @@
package org.julianfamily.keepassgo;
import android.app.Activity;
import android.content.ClipData;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
@@ -10,9 +11,11 @@ import android.util.Log;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
public final class SharedVaultImportActivity extends Activity {
private static final String TAG = "KeePassGOImport";
@@ -36,6 +39,7 @@ public final class SharedVaultImportActivity extends Activity {
}
private void handleIntent(Intent intent) {
logIntent(intent);
Uri uri = resolveSharedUri(intent);
if (uri == null) {
Log.i(TAG, "no shared vault URI on intent");
@@ -55,10 +59,29 @@ public final class SharedVaultImportActivity extends Activity {
}
String action = intent.getAction();
if (Intent.ACTION_SEND.equals(action)) {
return intent.getParcelableExtra(Intent.EXTRA_STREAM);
Uri extraStream = intent.getParcelableExtra(Intent.EXTRA_STREAM);
if (extraStream != null) {
return extraStream;
}
}
if (Intent.ACTION_SEND_MULTIPLE.equals(action)) {
ArrayList<Uri> streams = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
if (streams != null && !streams.isEmpty()) {
return streams.get(0);
}
}
if (Intent.ACTION_VIEW.equals(action)) {
return intent.getData();
Uri data = intent.getData();
if (data != null) {
return data;
}
}
ClipData clipData = intent.getClipData();
if (clipData != null && clipData.getItemCount() > 0) {
Uri clipUri = clipData.getItemAt(0).getUri();
if (clipUri != null) {
return clipUri;
}
}
return null;
}
@@ -69,7 +92,7 @@ public final class SharedVaultImportActivity extends Activity {
throw new IOException("failed to create " + dir.getAbsolutePath());
}
File pendingFile = new File(dir, "pending-shared-vault.kdbx");
try (InputStream in = getContentResolver().openInputStream(uri)) {
try (InputStream in = openSharedInputStream(uri)) {
if (in == null) {
throw new IOException("failed to open shared vault stream");
}
@@ -88,6 +111,17 @@ public final class SharedVaultImportActivity extends Activity {
}
}
private InputStream openSharedInputStream(Uri uri) throws IOException {
if ("file".equalsIgnoreCase(uri.getScheme())) {
String path = uri.getPath();
if (path == null || path.trim().isEmpty()) {
throw new IOException("file URI is missing a path");
}
return new FileInputStream(new File(path));
}
return getContentResolver().openInputStream(uri);
}
private String resolveDisplayName(Uri uri) {
String displayName = queryDisplayName(uri);
if (!displayName.isEmpty()) {
@@ -123,6 +157,20 @@ public final class SharedVaultImportActivity extends Activity {
return "";
}
private void logIntent(Intent intent) {
if (intent == null) {
return;
}
Log.i(TAG, "intent action=" + intent.getAction()
+ " type=" + intent.getType()
+ " data=" + intent.getData()
+ " flags=0x" + Integer.toHexString(intent.getFlags()));
ClipData clipData = intent.getClipData();
if (clipData != null) {
Log.i(TAG, "intent clip items=" + clipData.getItemCount());
}
}
private void launchMainActivity() {
Intent launch = new Intent();
launch.setClassName(this, "org.gioui.GioActivity");
+1 -1
View File
@@ -4,7 +4,7 @@ go 1.26
replace gioui.org => git.julianfamily.org/joejulian/gio-patched v0.9.1-0.20260416220049-9bfa6bc1c2dc
replace gioui.org/cmd => git.julianfamily.org/joejulian/gio-cmd-patched v0.9.1-0.20260416154203-192acd9d09b0
replace gioui.org/cmd => git.julianfamily.org/joejulian/gio-cmd-patched v0.9.1-0.20260417040456-1762d36ddecc
require (
gioui.org v0.9.0
+2 -2
View File
@@ -42,8 +42,8 @@ gioui.org/shader v1.0.8 h1:6ks0o/A+b0ne7RzEqRZK5f4Gboz2CfG+mVliciy6+qA=
gioui.org/shader v1.0.8/go.mod h1:mWdiME581d/kV7/iEhLmUgUK5iZ09XR5XpduXzbePVM=
gioui.org/x v0.8.0 h1:RhIlQNOFKKn8D8FeaKKaXCo7vB3x+fq4VcD10HW/YpA=
gioui.org/x v0.8.0/go.mod h1:aXtQb+kyqoUOjDl5/uMqAopjzVzMkeHBbMQOGT5KnSE=
git.julianfamily.org/joejulian/gio-cmd-patched v0.9.1-0.20260416154203-192acd9d09b0 h1:jZmX7CRuev3KrOW0yB2VQDtCrzRa4Q+qWcl3CGXq2sE=
git.julianfamily.org/joejulian/gio-cmd-patched v0.9.1-0.20260416154203-192acd9d09b0/go.mod h1:RBQfFU8JCgMjQ2wKU9DG3zMC38TnY97E5MKoBGhGl3s=
git.julianfamily.org/joejulian/gio-cmd-patched v0.9.1-0.20260417040456-1762d36ddecc h1:jyfCTx9wk/uLaEMkdKsg491C/kjfbG2EKAVTORhZxHo=
git.julianfamily.org/joejulian/gio-cmd-patched v0.9.1-0.20260417040456-1762d36ddecc/go.mod h1:RBQfFU8JCgMjQ2wKU9DG3zMC38TnY97E5MKoBGhGl3s=
git.julianfamily.org/joejulian/gio-patched v0.9.1-0.20260416220049-9bfa6bc1c2dc h1:p2AaZUAXa/ExPybNyeB05+GjTSZGA9lCfDpWz49IT5Y=
git.julianfamily.org/joejulian/gio-patched v0.9.1-0.20260416220049-9bfa6bc1c2dc/go.mod h1:BdI7mF5DCa3kxlo3G93XHL7khtZnk1gu4335pExk8gs=
git.wow.st/gmp/jni v0.0.0-20210610011705-34026c7e22d0 h1:bGG/g4ypjrCJoSvFrP5hafr9PPB5aw8SjcOWWila7ZI=