Compare commits

..

No commits in common. "ebfc751471da5972194169ac19a7c18da1e071e5" and "88e0634f093abc71db90ff597a9d6ae78d2ab70c" have entirely different histories.

7 changed files with 31 additions and 209 deletions

View File

@ -378,15 +378,7 @@ jobs:
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
ostp-gui/src-tauri/target/
key: cargo-windows-gui-${{ matrix.target }}-${{ hashFiles('**/Cargo.lock') }}
# Without a prefix fallback this cache NEVER restored on a release:
# cutting a release rewrites every Cargo.lock (version bump), which
# changes hashFiles(), which misses the exact key — so each release
# rebuilt every dependency from scratch. That is why the GUI jobs ran
# 2-4x longer than the plain release targets, which had this all along.
restore-keys: |
cargo-windows-gui-${{ matrix.target }}-
- name: Download wintun
shell: pwsh
@ -468,10 +460,7 @@ jobs:
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
ostp-gui/src-tauri/target/
key: cargo-linux-gui-${{ matrix.target }}-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
cargo-linux-gui-${{ matrix.target }}-
- name: Build Tauri App
working-directory: ostp-gui
@ -533,10 +522,7 @@ jobs:
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
ostp-gui/src-tauri/target/
key: cargo-macos-gui-${{ matrix.target }}-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
cargo-macos-gui-${{ matrix.target }}-
- name: Build Tauri App
working-directory: ostp-gui
@ -601,107 +587,27 @@ jobs:
with:
ndk-version: r26b
# The Android jobs had no Rust caching at all, so every release recompiled
# the whole ostp-jni dependency graph from scratch — the main reason these
# were among the slowest jobs in the matrix.
- name: Cache cargo
uses: actions/cache@v4
with:
path: |
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: cargo-android-${{ matrix.arch }}-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
cargo-android-${{ matrix.arch }}-
# cargo-ndk was built from source on every run. Cache the binary the same
# way the cross-compilation jobs already cache `cross`.
- name: Restore cargo-ndk binary cache
id: cargo-ndk-cache
uses: actions/cache@v4
with:
path: ~/.cargo/bin/cargo-ndk
key: cargo-ndk-bin-${{ runner.os }}-v1
- name: Install cargo-ndk (if not cached)
if: steps.cargo-ndk-cache.outputs.cache-hit != 'true'
run: cargo install cargo-ndk --locked
- name: Install cargo-ndk
run: cargo install cargo-ndk
- name: Build Android APK
shell: bash
working-directory: ostp-flutter
env:
OSTP_KEYSTORE_B64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
OSTP_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
OSTP_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
OSTP_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
run: |
set -euo pipefail
# 1. Materialise the upload keystore from secrets. Android keys an app
# by applicationId + signing key and refuses to update across a key
# change, so every published build MUST use this one key. Releases
# used to fall through to the per-machine debug keystore, which on
# ephemeral CI runners meant a different random key every build -
# hence "App not installed" on upgrade.
if [ -z "${OSTP_KEYSTORE_B64:-}" ]; then
echo "::error::ANDROID_KEYSTORE_BASE64 secret is not set. Refusing to publish a"
echo "::error::debug-signed APK: users could not update over it and the key is"
echo "::error::not reproducible. See docs for the one-time keystore setup."
exit 1
fi
export OSTP_KEYSTORE_PATH="$RUNNER_TEMP/ostp-upload.jks"
# Strip any stray CR/LF before decoding: the secret is pasted from a
# shell whose line endings we don't control, and a single trailing \r
# is enough to corrupt the decode.
printf '%s' "$OSTP_KEYSTORE_B64" | tr -d '\r\n' | base64 -d > "$OSTP_KEYSTORE_PATH"
# Verify the keystore opens BEFORE spending four minutes on Gradle only
# to fail at the packaging step. The size/SHA-256 are safe to print (a
# hash reveals nothing) and let the operator compare against the local
# file to tell a transport problem apart from a wrong password.
echo "keystore: $(stat -c%s "$OSTP_KEYSTORE_PATH") bytes, sha256 $(sha256sum "$OSTP_KEYSTORE_PATH" | cut -d' ' -f1)"
if ! keytool -list -keystore "$OSTP_KEYSTORE_PATH" \
-storepass "$OSTP_KEYSTORE_PASSWORD" >/dev/null 2>&1; then
echo "::error::The keystore did not open with ANDROID_KEYSTORE_PASSWORD."
echo "::error::If the SHA-256 above matches your local ostp-upload.jks, the file"
echo "::error::arrived intact and the password secret itself is wrong - note that"
echo "::error::PowerShell expands \$ inside double quotes, so a password containing"
echo "::error::one gets mangled unless it was set with single quotes."
exit 1
fi
if ! keytool -list -keystore "$OSTP_KEYSTORE_PATH" \
-storepass "$OSTP_KEYSTORE_PASSWORD" -alias "$OSTP_KEY_ALIAS" >/dev/null 2>&1; then
echo "::error::Keystore opened, but it has no key under ANDROID_KEY_ALIAS."
echo "::error::Aliases present in the keystore:"
keytool -list -keystore "$OSTP_KEYSTORE_PATH" -storepass "$OSTP_KEYSTORE_PASSWORD" \
| grep -i "PrivateKeyEntry" || true
exit 1
fi
# 2. Compile JNI
# 1. Compile JNI
mkdir -p android/app/src/main/jniLibs/${{ matrix.arch }}
cd ../ostp-jni
cargo ndk -t ${{ matrix.arch }} -o "../ostp-flutter/android/app/src/main/jniLibs" build --release
cd ../ostp-flutter
# 3. Build Flutter APK
flutter build apk --release --target-platform ${{ matrix.flutter_target }}
# 4. Fail loudly if the APK somehow still came out debug-signed, rather
# than shipping another un-updatable build.
APK=build/app/outputs/flutter-apk/app-release.apk
if "$ANDROID_HOME"/build-tools/*/apksigner verify --print-certs "$APK" 2>/dev/null \
| grep -qi "CN=Android Debug"; then
echo "::error::APK is signed with the Android debug certificate - aborting."
exit 1
fi
# 5. Copy to output
cp "$APK" ostp-android-${{ matrix.arch }}.apk
# 4. Copy to output
cp build/app/outputs/flutter-apk/app-release.apk ostp-android-${{ matrix.arch }}.apk
- name: Upload to GitHub Release
uses: softprops/action-gh-release@v2

8
.gitignore vendored
View File

@ -26,13 +26,6 @@ test_route.ps1
config.json
wintun.dll
# Android signing keys. The upload keystore is the ONE key every published APK
# must be signed with (Android refuses to update an app across a key change),
# so losing or leaking it is unrecoverable — it can never be committed.
*.jks
*.keystore
key.properties
# Server runtime cache (public IP autodetect) — must never be committed,
# it's regenerated locally and leaks whatever host it ran on last.
.ostp_public_ip
@ -46,7 +39,6 @@ turn-harvesting-idea.md
# Private tooling (closed-source)
ostp-prober/
ostp-lab/
ostp-brain/

View File

@ -1,6 +1,6 @@
{
"target_version": "0.4.2",
"branch": "master",
"branch": "beta",
"alpha_iteration": 0,
"beta_iteration": 5
"beta_iteration": 4
}

View File

@ -1,6 +1,3 @@
import java.io.FileInputStream
import java.util.Properties
plugins {
id("com.android.application")
id("kotlin-android")
@ -8,37 +5,6 @@ plugins {
id("dev.flutter.flutter-gradle-plugin")
}
// ── Release signing material ────────────────────────────────────────────────
// Supplied out-of-band and never committed: either an `android/key.properties`
// file (local release builds) or OSTP_KEYSTORE_* environment variables (CI).
//
// This exists because the release build used to be signed with the DEBUG
// keystore (the stock Flutter template TODO). Android identifies an app by
// applicationId + signing key, and refuses to update across a key change. The
// debug keystore is auto-generated per machine, and CI runners are ephemeral,
// so every published build carried a different random key — which is why
// updating on top of a previous install failed with "App not installed" /
// "unable to parse the package" and only a full uninstall+reinstall worked.
val keystoreProperties = Properties().apply {
val propsFile = rootProject.file("key.properties")
if (propsFile.exists()) {
FileInputStream(propsFile).use { load(it) }
}
}
// Blank counts as absent. GitHub Actions substitutes an EMPTY STRING (not an
// unset variable) for a secret that doesn't exist, so `getenv(...) ?: fallback`
// silently kept the empty value — the elvis operator only catches null. That is
// how an unset ANDROID_KEY_PASSWORD ended up being used as the literal key
// password instead of falling back to the store password, producing Gradle's
// "Get Key failed: Given final block not properly padded".
fun signingSetting(propKey: String, envKey: String): String? =
(keystoreProperties.getProperty(propKey) ?: System.getenv(envKey))
?.takeIf { it.isNotBlank() }
val releaseStorePath: String? = signingSetting("storeFile", "OSTP_KEYSTORE_PATH")
val hasReleaseSigning: Boolean = !releaseStorePath.isNullOrBlank()
android {
namespace = "com.ospab.ostp_client"
compileSdk = flutter.compileSdkVersion
@ -68,43 +34,11 @@ android {
}
}
signingConfigs {
create("release") {
if (hasReleaseSigning) {
val store = signingSetting("storePassword", "OSTP_KEYSTORE_PASSWORD")
storeFile = file(releaseStorePath!!)
storePassword = store
keyAlias = signingSetting("keyAlias", "OSTP_KEY_ALIAS")
// PKCS12 (the keytool default since Java 9, and what our upload
// keystore is) cannot hold a key password that differs from the
// store password — the format simply has no place to put one. So
// treat a missing key password as "same as the store password"
// instead of demanding a secret that, for this keystore, can only
// ever be a duplicate. An explicit value still wins, for the older
// JKS format where the two genuinely can differ.
keyPassword = signingSetting("keyPassword", "OSTP_KEY_PASSWORD") ?: store
}
}
}
buildTypes {
release {
// Use the real upload key when one was supplied; otherwise fall back to
// the debug keystore so a plain local `flutter build apk --release`
// still works for development. Anything PUBLISHED must take the first
// branch — a debug-signed build cannot be updated over, and its key is
// machine-local, so it also can't be reproduced later.
if (hasReleaseSigning) {
signingConfig = signingConfigs.getByName("release")
} else {
logger.warn(
"OSTP: no release keystore configured (android/key.properties or " +
"OSTP_KEYSTORE_PATH) - falling back to the DEBUG keystore. This APK " +
"is for local use only: users cannot update over it, and the key is " +
"not reproducible on another machine."
)
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
}
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}

View File

@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 0.4.2+25
version: 0.4.2+23
environment:
sdk: ^3.11.4

View File

@ -17,12 +17,10 @@
--c-accent-dim: rgba(var(--c-fg-rgb),0.08);
--c-accent-glow: rgba(var(--c-fg-rgb),0.18);
/* Green only for "connected" state the one deliberate break from the
monochrome palette, so a successful connection reads at a glance. */
--c-green-rgb: 46, 230, 109;
--c-green: #2ee66d;
--c-green-glow: rgba(var(--c-green-rgb),0.28);
--c-green-dim: rgba(var(--c-green-rgb),0.09);
/* Green only for "connected" state */
--c-green: #e8e8e8;
--c-green-glow: rgba(232,232,232,0.25);
--c-green-dim: rgba(232,232,232,0.07);
--c-red: #ff5f5f;
--c-amber: #f0b840;
@ -57,11 +55,9 @@
--c-accent: #18181b;
--c-accent-dim: rgba(0,0,0,0.08);
--c-accent-glow: rgba(0,0,0,0.14);
/* Deeper green so it stays legible against the light background. */
--c-green-rgb: 22, 163, 74;
--c-green: #16a34a;
--c-green-glow: rgba(var(--c-green-rgb),0.22);
--c-green-dim: rgba(var(--c-green-rgb),0.08);
--c-green: #18181b;
--c-green-glow: rgba(0,0,0,0.16);
--c-green-dim: rgba(0,0,0,0.05);
--c-red: #dc2626;
--c-amber: #d97706;
--c-txt-1: #18181b;
@ -159,7 +155,7 @@ a { text-decoration: none; }
transition: background var(--t-med), box-shadow var(--t-med);
}
.brand-dot.connecting { animation: dot-blink 1.4s infinite ease-in-out; background: var(--c-accent); }
.brand-dot.connected { background: var(--c-green); box-shadow: 0 0 10px var(--c-green-glow); }
.brand-dot.connected { background: var(--c-accent); box-shadow: 0 0 10px var(--c-accent-glow); }
@keyframes dot-blink {
0%,100% { opacity: 1; }
@ -237,11 +233,11 @@ a { text-decoration: none; }
.orbit-wrap.connected .orbit {
animation: orbit-spin 4s linear infinite;
border-color: rgba(var(--c-green-rgb),0.30);
border-color: rgba(var(--c-fg-rgb),0.14);
opacity: 1;
}
.orbit-wrap.connected .orbit-2 { animation-duration: 6s; animation-direction: reverse; border-color: rgba(var(--c-green-rgb),0.18); }
.orbit-wrap.connected .orbit-3 { animation-duration: 9s; border-color: rgba(var(--c-green-rgb),0.10); }
.orbit-wrap.connected .orbit-2 { animation-duration: 6s; animation-direction: reverse; border-color: rgba(var(--c-fg-rgb),0.08); }
.orbit-wrap.connected .orbit-3 { animation-duration: 9s; border-color: rgba(var(--c-fg-rgb),0.04); }
@keyframes orbit-spin {
from { transform: rotate(0deg); }
@ -274,9 +270,9 @@ a { text-decoration: none; }
animation: btn-breathe 2s infinite ease-in-out;
}
.power-btn.connected {
border-color: var(--c-green);
color: var(--c-green);
box-shadow: 0 0 0 8px var(--c-green-dim), 0 0 50px var(--c-green-glow), 0 8px 32px rgba(0,0,0,0.5);
border-color: rgba(var(--c-fg-rgb),0.8);
color: var(--c-txt-1);
box-shadow: 0 0 0 8px rgba(var(--c-fg-rgb),0.04), 0 0 50px rgba(var(--c-fg-rgb),0.12), 0 8px 32px rgba(0,0,0,0.5);
}
.power-btn.error {
border-color: var(--c-red);

View File

@ -3,6 +3,7 @@ use clap::Parser;
use std::fs;
use std::path::PathBuf;
use colored::Colorize;
use sha2::Digest;
#[derive(Parser, Debug)]
#[command(author, version, about = "OSTP Core - Ospab Stealth Transport Protocol", long_about = None)]
@ -724,14 +725,7 @@ fn run_setup_wizard(config_path: &std::path::Path) -> Result<()> {
// this used to be a DefaultHasher (SipHash) placeholder that produced a
// differently-shaped digest, so a password set up through this wizard could
// never actually log into the panel it just configured.
// Trait-qualified so this compiles whether or not `sha2::Digest` happens
// to be in scope: `digest` is a trait method, and relying on the import
// alone broke the CI build once (v0.4.2-beta.3) while resolving fine
// locally.
let pass_hash = format!(
"{:x}",
<sha2::Sha256 as sha2::Digest>::digest(password.as_bytes())
);
let pass_hash = format!("{:x}", sha2::Sha256::digest(password.as_bytes()));
wizard_step(4, TOTAL, "Saving configuration");
let panel_bind = format!("0.0.0.0:{}", panel_port);