# PSG Launcher An all-in-one launcher for PSG tools. Fetches a signed manifest from your Gitea instance, displays available apps, and handles install/update/launch — with OTA updates for both the launcher itself and every app it manages. --- ## Security model | Threat | Mitigation | |---|---| | MITM on the wire | All fetches use `https_only(true)` — plain HTTP is rejected at the Rust layer | | Tampered manifest | `apps.json` has a detached ed25519 signature (`apps.json.sig`); the launcher verifies it before parsing | | Tampered package | Each app package has an inline SHA-256 hash **and** a separate ed25519 signature; both are verified before writing a single byte to disk | | Rollback / replay | Tauri's self-updater rejects packages whose version ≤ the running version | | Compromised CDN / server | The private key never touches the server; only signed content is accepted | | Supply chain (npm) | Frontend is a thin React shell; all crypto lives in Rust with audited crates | **Two separate keys are used:** - `manifest-private.pem` — signs `apps.json` and each app package (your custom key) - Tauri signer key — signs the launcher installer for self-updates (generated by Tauri CLI) --- ## Prerequisites | Tool | Version | |---|---| | Rust | 1.77+ | | Node.js | 20+ | | OpenSSL | 3.x (`winget install ShiningLight.OpenSSL.Light`) | | WebView2 | Bundled with Windows 11 | --- ## First-time setup ### 1 — Generate the manifest signing keypair ```powershell .\scripts\keygen.ps1 ``` This writes `keys\manifest-private.pem` and `keys\manifest-public.pem`. Copy the printed base64 value into `src-tauri\src\config.rs` as `MANIFEST_PUBLIC_KEY_B64`. > ⚠ `keys\` is in `.gitignore`. Never commit the private key. ### 2 — Generate the Tauri self-updater keypair ```powershell npm run tauri signer generate -- -w keys\tauri.key ``` Copy the printed public key into `src-tauri\tauri.conf.json` under `plugins.updater.pubkey`. ### 3 — Update config.rs Open `src-tauri\src\config.rs` and replace the three `YOURDOMAIN / OWNER` placeholders with your actual Gitea instance URL and repository owner name. ### 4 — Update tauri.conf.json Replace the `YOURDOMAIN / OWNER` placeholders in `src-tauri\tauri.conf.json`. ### 5 — Install npm dependencies ```powershell npm install ``` --- ## Development ```powershell npm run tauri dev ``` The Tauri dev server starts Vite at `http://localhost:1420` and hot-reloads the UI. --- ## Building a release locally ```powershell npm run tauri build ``` Installer goes to `src-tauri\target\release\bundle\nsis\`. --- ## Adding an app to the manifest 1. Build your app and note its `.exe` / `.zip` path. 2. Sign the package and get its hash + signature: ```powershell .\scripts\sign-package.ps1 -PackagePath .\path\to\your-app.exe ``` 3. Copy the printed values into `manifests\apps.json` (add a new entry or update `current_version`). 4. Re-sign the manifest: ```powershell .\scripts\sign-manifest.ps1 ``` 5. Commit, push, and tag: ```powershell git add manifests\ git commit -m "feat: add your-app v1.0.0" git tag v0.2.0 git push && git push --tags ``` The release workflow triggers automatically, builds the launcher, and pushes the updated manifests to the `releases` branch where the launcher can reach them. --- ## CI secrets (Gitea repository settings → Secrets) | Secret | Value | |---|---| | `TAURI_SIGNING_PRIVATE_KEY` | Content of `keys\tauri.key` | | `TAURI_SIGNING_PRIVATE_KEY_PASSWORD` | Passphrase (or empty string) | | `MANIFEST_SIGNING_KEY` | Content of `keys\manifest-private.pem` | | `GITEA_TOKEN` | Personal access token with `repo` write scope | --- ## Manifest structure ### `manifests/apps.json` ```jsonc { "schema_version": 1, "generated_at": "ISO 8601 timestamp", "minimum_launcher_version": "0.1.0", "apps": [ { "id": "my-app", // stable, URL-safe ID "name": "My App", "description": "...", "category": "tools", // tools | utilities | games | media | development | network | other "current_version": "1.0.0", "icon_url": null, // optional HTTPS URL "changelog": "...", "tags": [], "platforms": { "windows-x86_64": { "download_url": "https://...", "hash_sha256": "lowercase hex", "size_bytes": 1234567, "install_type": "portable", // portable | zip | installer "install_path": null, // null = default (%APPDATA%\PSG\) "signature": "base64 ed25519 sig of download bytes" } } } ] } ``` ### `manifests/apps.json.sig` Plain text file containing the base64-encoded ed25519 signature of the raw `apps.json` bytes. ### `manifests/latest.json` Tauri's self-update format — updated automatically by the release workflow. --- ## Project layout ``` PSG-Conduit/ ├── src/ React + TypeScript frontend │ ├── components/ UI components (AppCard, Sidebar, TitleBar, …) │ ├── hooks/ useAppManifest, useLauncherUpdate │ ├── lib/commands.ts Type-safe Tauri invoke wrappers │ └── types/manifest.ts Shared TypeScript types ├── src-tauri/ Rust backend │ ├── src/ │ │ ├── commands/ Tauri commands (manifest, apps, updater) │ │ ├── models/ Serde types (manifest, installed) │ │ ├── config.rs Public key + manifest URLs │ │ └── error.rs Serialisable error enum │ ├── tauri.conf.json │ └── capabilities/ ├── manifests/ Cloud manifests (committed to `releases` branch via CI) │ ├── apps.json │ ├── apps.json.sig ← generated by sign-manifest.ps1 │ └── latest.json ← generated by release workflow ├── scripts/ │ ├── keygen.ps1 Generate ed25519 keypair │ ├── sign-manifest.ps1 Sign apps.json → apps.json.sig │ └── sign-package.ps1 Get hash + sig for a single app package └── .gitea/workflows/ └── release.yml Build + sign + publish on tag push ```