From 3f28c3fdb465e5eb887d48187b9e902cd0860d28 Mon Sep 17 00:00:00 2001 From: SkateSpots Date: Sun, 1 Nov 2026 17:22:15 +0700 Subject: [PATCH] security: rotate CVM identity and externalize signing --- AGENTS.md | 4 ++-- ZAPSTORE.md | 19 +++++-------------- scripts/inject-signing-config.mjs | 23 +++++++++++++++++------ src/hooks/useCVMServerHealth.ts | 2 +- src/pages/AboutPage.tsx | 2 +- vite.config.ts | 20 +++++++++++++++++++- 6 files changed, 45 insertions(+), 25 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 0ee61a7..d3edbc0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2405,7 +2405,7 @@ The committed `package-lock.json` (from commit `f25fc2b`) had entries stripped o ### Android release build - `npm run android:build` → `android/app/build/outputs/apk/release/app-release.apk`. - **Versioning**: `package.json` `version` is the single source of truth; `scripts/sync-android-version.mjs` syncs it to `versionName`/`versionCode` (e.g. `1.4.5` → `10405`) during the build. Bump **only** `package.json`. -- **Signing**: secrets live in the gitignored `keystore.properties`; `scripts/inject-signing-config.mjs` injects a managed block into `android/app/build.gradle` (runs in `android:init`/`android:sync`/`android:build`). The authoritative keystore is **`/home/sovereignsk8/skatespots-release.jks`** (alias `skatespots`). An older repo-root copy had a wrong password and was retired to `~/.backups/keystore/`. +- **Signing**: secrets live **outside the repo** at `~/.config/skatespots/android/keystore.properties`; `scripts/inject-signing-config.mjs` injects a managed block into `android/app/build.gradle` (runs in `android:init`/`android:sync`/`android:build`). The authoritative keystore is **`~/.config/skatespots/android/skatespots-release.jks`** (alias `skatespots`, mode 600). An older repo-root copy had a wrong password and was retired to `~/.backups/keystore/`. Never put signing material back in the repo: Vite serves files by filesystem path, not by `.gitignore`. - `android/` is **generated and gitignored**; recreate with `npm run android:init`. - **Launch splash:** `resources/splash.png` is the orange brand wheel. Capacitor's generated drawables used a stock blue placeholder, so `scripts/make-android-splash.mjs` @@ -3431,7 +3431,7 @@ The community relay moved off the hosted relay.tools instance - `git push` may print a benign `cannot lock ref ... incorrect old value provided` retry warning; verify with `git fetch origin && git rev-list --left-right --count origin/main...HEAD`. ### Secrets — never commit or paste -`keystore.properties`, `*.jks`, `.env*`, and `android/` are gitignored. Never paste the nsec or keystore password into chat, logs, or command history — keep them in a password manager. +Signing material (`keystore.properties`, `*.jks`) lives **outside the repo** at `~/.config/skatespots/android/`; `*.jks`, `.env*`, and `android/` remain gitignored as a safety net. Never paste the nsec or keystore password into chat, logs, or command history — keep them in a password manager. Do not bind the Vite dev server to the LAN; use `adb reverse` for device testing. ## Project Structure diff --git a/ZAPSTORE.md b/ZAPSTORE.md index 0c75f75..716379f 100644 --- a/ZAPSTORE.md +++ b/ZAPSTORE.md @@ -156,16 +156,18 @@ Or use Android Studio: right-click `app` → New → Image Asset → import `pub ## Step 5 — Generate a signing keystore (one-time) ```bash +install -d -m 700 ~/.config/skatespots/android keytool -genkey -v \ - -keystore skatespots-release.jks \ + -keystore ~/.config/skatespots/android/skatespots-release.jks \ -alias skatespots \ -keyalg RSA \ -keysize 2048 \ -validity 10000 \ -dname "CN=SkateSpots, OU=SkateSpots, O=SkateSpots, L=Bangkok, S=Bangkok, C=TH" +chmod 600 ~/.config/skatespots/android/skatespots-release.jks ``` -**Save the keystore file and passwords somewhere safe — you need them for every future release.** +Put the passwords in `~/.config/skatespots/android/keystore.properties` (mode 600), not in the repo. **Save the keystore file and passwords somewhere safe — you need them for every future release.** --- @@ -175,18 +177,7 @@ keytool -genkey -v \ npm run android:build ``` -Or manually with signing: - -```bash -npm run build -npx cap sync android -cd android -./gradlew assembleRelease \ - -Pandroid.injected.signing.store.file=../skatespots-release.jks \ - -Pandroid.injected.signing.store.password=YOUR_STORE_PASSWORD \ - -Pandroid.injected.signing.key.alias=skatespots \ - -Pandroid.injected.signing.key.password=YOUR_KEY_PASSWORD -``` +`npm run android:build` injects the external signing config automatically. Do **not** pass signing passwords as `-Pandroid.injected...` command-line properties; that exposes them in shell history and process listings. APK output: `android/app/build/outputs/apk/release/app-release.apk` diff --git a/scripts/inject-signing-config.mjs b/scripts/inject-signing-config.mjs index 0c3b10f..fabe7bd 100644 --- a/scripts/inject-signing-config.mjs +++ b/scripts/inject-signing-config.mjs @@ -15,7 +15,12 @@ * 1. Removes any previously managed signing block. * 2. Removes a legacy hardcoded signingConfigs block (inline passwords). * 3. Writes a managed signingConfigs block into android/app/build.gradle that - * reads secrets from the gitignored keystore.properties at the repo root. + * reads secrets from a keystore.properties file outside the repo. + * + * The signing material is deliberately outside the Vite project root: + * ~/.config/skatespots/android/keystore.properties + * ~/.config/skatespots/android/skatespots-release.jks + * Override the properties path with SKATESPOTS_KEYSTORE_PROPERTIES. * * It exits quietly (0) when android/ isn't generated yet, or when * keystore.properties is absent — so debug-only builds without a keystore still @@ -23,11 +28,16 @@ */ import { readFileSync, writeFileSync, existsSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; -import { dirname, join } from 'node:path'; +import { dirname, join, resolve } from 'node:path'; +import { homedir } from 'node:os'; const root = join(dirname(fileURLToPath(import.meta.url)), '..'); const buildGradle = join(root, 'android', 'app', 'build.gradle'); -const keystoreProps = join(root, 'keystore.properties'); +const keystoreProps = resolve( + process.env.SKATESPOTS_KEYSTORE_PROPERTIES ?? + join(homedir(), '.config', 'skatespots', 'android', 'keystore.properties'), +); +const gradleString = (value) => value.replace(/\\/g, '\\\\').replace(/'/g, "\\'"); const BEGIN = '// >>> managed by scripts/inject-signing-config.mjs'; const END = '// <<< managed by scripts/inject-signing-config.mjs <<<'; @@ -43,10 +53,11 @@ if (!existsSync(keystoreProps)) { const managedLines = [ `${BEGIN} >>>`, - '// Release signing is read from keystore.properties at the repo root', - '// (gitignored). Edit that file, not this block.', + '// Release signing is read from keystore.properties outside the repo.', + '// Edit that file, not this block.', 'def _keystoreProps = new Properties()', - "def _keystorePropsFile = rootProject.file('../keystore.properties')", + "def _keystorePropsPath = System.getenv('SKATESPOTS_KEYSTORE_PROPERTIES')", + `def _keystorePropsFile = new File(_keystorePropsPath ?: '${gradleString(keystoreProps)}')`, 'if (_keystorePropsFile.exists()) {', ' _keystorePropsFile.withInputStream { _keystoreProps.load(it) }', '}', diff --git a/src/hooks/useCVMServerHealth.ts b/src/hooks/useCVMServerHealth.ts index 128d319..ca497d5 100644 --- a/src/hooks/useCVMServerHealth.ts +++ b/src/hooks/useCVMServerHealth.ts @@ -20,7 +20,7 @@ import { useNostr } from '@nostrify/react'; /** The SkateSpots CVM server identity (see contextvm.org/s/). */ export const CVM_SERVER_PUBKEY = - 'e3564bb877adb0c99a7e4973ccb22e516d7d9eca0027d0bd62a04174ed670530'; + '940bf0e4fbc64b6c0f09d48c3264723c524a31b5ad0d1aaec361263731689d95'; /** The relay the CVM server announces on. */ export const CVM_RELAY = 'wss://relay.contextvm.org'; diff --git a/src/pages/AboutPage.tsx b/src/pages/AboutPage.tsx index 636b0c9..c77e626 100644 --- a/src/pages/AboutPage.tsx +++ b/src/pages/AboutPage.tsx @@ -279,7 +279,7 @@ Version: v0.1.0`; { label: t('settingsUi.about.creditMapData'), name: 'BTCMap', url: 'https://btcmap.org/', desc: t('settingsUi.about.creditBtcmap') }, { label: t('settingsUi.about.creditCrowdfunding'), name: 'Angor', url: 'https://angor.io/project/angor1qtunna00uqyx52rr0nvqa059z2gknhrmn7urd6z', desc: t('settingsUi.about.creditAngor') }, { label: t('settingsUi.about.creditDistributed'), name: 'Zapstore', url: 'https://zapstore.dev/apps/org.skatespots.app', desc: t('settingsUi.about.creditZapstore') }, - { label: t('settingsUi.about.creditTrust'), name: 'ContextVM', url: 'https://contextvm.org/s/e3564bb877adb0c99a7e4973ccb22e516d7d9eca0027d0bd62a04174ed670530', desc: t('settingsUi.about.creditContextvm') }, + { label: t('settingsUi.about.creditTrust'), name: 'ContextVM', url: 'https://contextvm.org/s/940bf0e4fbc64b6c0f09d48c3264723c524a31b5ad0d1aaec361263731689d95', desc: t('settingsUi.about.creditContextvm') }, ].map((item) => ( ({ // The regular web deploy keeps '/' (served from a domain root). base: process.env.CAPACITOR_BUILD ? './' : '/', server: { - host: "::", + // Keep the dev server off the LAN. The phone preview uses localhost, and + // native testing should use `adb reverse` rather than exposing Vite. + host: "127.0.0.1", port: 8080, + fs: { + strict: true, + // Defence in depth: secrets live outside the repo, but never serve them + // even if someone temporarily copies one back in. + deny: [ + '.env', + '.env.*', + '*.{crt,pem}', + '**/.git/**', + '**/keystore.properties', + '**/*.jks', + '**/*.keystore', + '**/*.p12', + '**/*.key', + ], + }, // Dev proxy for the MintRadar directory (no CORS upstream; see public/_redirects). proxy: { '/api/mints': {