security: rotate CVM identity and externalize signing
Some checks are pending
Deploy to GitHub Pages / deploy (push) Waiting to run

This commit is contained in:
SkateSpots 2026-11-01 17:22:15 +07:00
parent ab90e02f69
commit 3f28c3fdb4
6 changed files with 45 additions and 25 deletions

View file

@ -2405,7 +2405,7 @@ The committed `package-lock.json` (from commit `f25fc2b`) had entries stripped o
### Android release build ### Android release build
- `npm run android:build` → `android/app/build/outputs/apk/release/app-release.apk`. - `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`. - **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`. - `android/` is **generated and gitignored**; recreate with `npm run android:init`.
- **Launch splash:** `resources/splash.png` is the orange brand wheel. Capacitor's - **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` 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`. - `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 ### 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 ## Project Structure

View file

@ -156,16 +156,18 @@ Or use Android Studio: right-click `app` → New → Image Asset → import `pub
## Step 5 — Generate a signing keystore (one-time) ## Step 5 — Generate a signing keystore (one-time)
```bash ```bash
install -d -m 700 ~/.config/skatespots/android
keytool -genkey -v \ keytool -genkey -v \
-keystore skatespots-release.jks \ -keystore ~/.config/skatespots/android/skatespots-release.jks \
-alias skatespots \ -alias skatespots \
-keyalg RSA \ -keyalg RSA \
-keysize 2048 \ -keysize 2048 \
-validity 10000 \ -validity 10000 \
-dname "CN=SkateSpots, OU=SkateSpots, O=SkateSpots, L=Bangkok, S=Bangkok, C=TH" -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 npm run android:build
``` ```
Or manually with signing: `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.
```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
```
APK output: `android/app/build/outputs/apk/release/app-release.apk` APK output: `android/app/build/outputs/apk/release/app-release.apk`

View file

@ -15,7 +15,12 @@
* 1. Removes any previously managed signing block. * 1. Removes any previously managed signing block.
* 2. Removes a legacy hardcoded signingConfigs block (inline passwords). * 2. Removes a legacy hardcoded signingConfigs block (inline passwords).
* 3. Writes a managed signingConfigs block into android/app/build.gradle that * 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 * It exits quietly (0) when android/ isn't generated yet, or when
* keystore.properties is absent — so debug-only builds without a keystore still * keystore.properties is absent — so debug-only builds without a keystore still
@ -23,11 +28,16 @@
*/ */
import { readFileSync, writeFileSync, existsSync } from 'node:fs'; import { readFileSync, writeFileSync, existsSync } from 'node:fs';
import { fileURLToPath } from 'node:url'; 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 root = join(dirname(fileURLToPath(import.meta.url)), '..');
const buildGradle = join(root, 'android', 'app', 'build.gradle'); 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 BEGIN = '// >>> managed by scripts/inject-signing-config.mjs';
const END = '// <<< 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 = [ const managedLines = [
`${BEGIN} >>>`, `${BEGIN} >>>`,
'// Release signing is read from keystore.properties at the repo root', '// Release signing is read from keystore.properties outside the repo.',
'// (gitignored). Edit that file, not this block.', '// Edit that file, not this block.',
'def _keystoreProps = new Properties()', '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()) {', 'if (_keystorePropsFile.exists()) {',
' _keystorePropsFile.withInputStream { _keystoreProps.load(it) }', ' _keystorePropsFile.withInputStream { _keystoreProps.load(it) }',
'}', '}',

View file

@ -20,7 +20,7 @@ import { useNostr } from '@nostrify/react';
/** The SkateSpots CVM server identity (see contextvm.org/s/<pubkey>). */ /** The SkateSpots CVM server identity (see contextvm.org/s/<pubkey>). */
export const CVM_SERVER_PUBKEY = export const CVM_SERVER_PUBKEY =
'e3564bb877adb0c99a7e4973ccb22e516d7d9eca0027d0bd62a04174ed670530'; '940bf0e4fbc64b6c0f09d48c3264723c524a31b5ad0d1aaec361263731689d95';
/** The relay the CVM server announces on. */ /** The relay the CVM server announces on. */
export const CVM_RELAY = 'wss://relay.contextvm.org'; export const CVM_RELAY = 'wss://relay.contextvm.org';

View file

@ -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.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.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.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) => ( ].map((item) => (
<a <a
key={item.name} key={item.name}

View file

@ -12,8 +12,26 @@ export default defineConfig(() => ({
// The regular web deploy keeps '/' (served from a domain root). // The regular web deploy keeps '/' (served from a domain root).
base: process.env.CAPACITOR_BUILD ? './' : '/', base: process.env.CAPACITOR_BUILD ? './' : '/',
server: { 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, 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). // Dev proxy for the MintRadar directory (no CORS upstream; see public/_redirects).
proxy: { proxy: {
'/api/mints': { '/api/mints': {