mirror of https://github.com/ospab/ostp.git
Compare commits
9 Commits
88e0634f09
...
ebfc751471
| Author | SHA1 | Date |
|---|---|---|
|
|
ebfc751471 | |
|
|
3cda1a9bd4 | |
|
|
77a45d7642 | |
|
|
6abae68f35 | |
|
|
cb57347d51 | |
|
|
32c36afc3b | |
|
|
a8aba8f4b8 | |
|
|
2ede607027 | |
|
|
0c69617725 |
|
|
@ -378,7 +378,15 @@ 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
|
||||
|
|
@ -460,7 +468,10 @@ 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
|
||||
|
|
@ -522,7 +533,10 @@ 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
|
||||
|
|
@ -587,27 +601,107 @@ jobs:
|
|||
with:
|
||||
ndk-version: r26b
|
||||
|
||||
- name: Install cargo-ndk
|
||||
run: cargo install cargo-ndk
|
||||
# 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: 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: |
|
||||
# 1. Compile JNI
|
||||
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
|
||||
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. Copy to output
|
||||
cp build/app/outputs/flutter-apk/app-release.apk ostp-android-${{ matrix.arch }}.apk
|
||||
# 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
|
||||
|
||||
- name: Upload to GitHub Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
|
|
|
|||
|
|
@ -26,6 +26,13 @@ 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
|
||||
|
|
@ -39,6 +46,7 @@ turn-harvesting-idea.md
|
|||
|
||||
# Private tooling (closed-source)
|
||||
ostp-prober/
|
||||
ostp-lab/
|
||||
|
||||
ostp-brain/
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"target_version": "0.4.2",
|
||||
"branch": "beta",
|
||||
"branch": "master",
|
||||
"alpha_iteration": 0,
|
||||
"beta_iteration": 4
|
||||
"beta_iteration": 5
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
import java.io.FileInputStream
|
||||
import java.util.Properties
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("kotlin-android")
|
||||
|
|
@ -5,6 +8,37 @@ 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
|
||||
|
|
@ -34,11 +68,43 @@ 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 {
|
||||
// TODO: Add your own signing config for the release build.
|
||||
// Signing with the debug keys for now, so `flutter run --release` works.
|
||||
// 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."
|
||||
)
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
}
|
||||
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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+23
|
||||
version: 0.4.2+25
|
||||
|
||||
environment:
|
||||
sdk: ^3.11.4
|
||||
|
|
|
|||
|
|
@ -17,10 +17,12 @@
|
|||
--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 */
|
||||
--c-green: #e8e8e8;
|
||||
--c-green-glow: rgba(232,232,232,0.25);
|
||||
--c-green-dim: rgba(232,232,232,0.07);
|
||||
/* 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);
|
||||
|
||||
--c-red: #ff5f5f;
|
||||
--c-amber: #f0b840;
|
||||
|
|
@ -55,9 +57,11 @@
|
|||
--c-accent: #18181b;
|
||||
--c-accent-dim: rgba(0,0,0,0.08);
|
||||
--c-accent-glow: rgba(0,0,0,0.14);
|
||||
--c-green: #18181b;
|
||||
--c-green-glow: rgba(0,0,0,0.16);
|
||||
--c-green-dim: rgba(0,0,0,0.05);
|
||||
/* 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-red: #dc2626;
|
||||
--c-amber: #d97706;
|
||||
--c-txt-1: #18181b;
|
||||
|
|
@ -155,7 +159,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-accent); box-shadow: 0 0 10px var(--c-accent-glow); }
|
||||
.brand-dot.connected { background: var(--c-green); box-shadow: 0 0 10px var(--c-green-glow); }
|
||||
|
||||
@keyframes dot-blink {
|
||||
0%,100% { opacity: 1; }
|
||||
|
|
@ -233,11 +237,11 @@ a { text-decoration: none; }
|
|||
|
||||
.orbit-wrap.connected .orbit {
|
||||
animation: orbit-spin 4s linear infinite;
|
||||
border-color: rgba(var(--c-fg-rgb),0.14);
|
||||
border-color: rgba(var(--c-green-rgb),0.30);
|
||||
opacity: 1;
|
||||
}
|
||||
.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); }
|
||||
.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); }
|
||||
|
||||
@keyframes orbit-spin {
|
||||
from { transform: rotate(0deg); }
|
||||
|
|
@ -270,9 +274,9 @@ a { text-decoration: none; }
|
|||
animation: btn-breathe 2s infinite ease-in-out;
|
||||
}
|
||||
.power-btn.connected {
|
||||
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);
|
||||
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);
|
||||
}
|
||||
.power-btn.error {
|
||||
border-color: var(--c-red);
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ 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)]
|
||||
|
|
@ -725,7 +724,14 @@ 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.
|
||||
let pass_hash = format!("{:x}", sha2::Sha256::digest(password.as_bytes()));
|
||||
// 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())
|
||||
);
|
||||
|
||||
wizard_step(4, TOTAL, "Saving configuration");
|
||||
let panel_bind = format!("0.0.0.0:{}", panel_port);
|
||||
|
|
|
|||
Loading…
Reference in New Issue