Files
smirk-backend-core/tools/secret-scan.sh
T
jw 81a3e4cf58 fix: pre-ship audit remediation for auth identity, abuse control and retention
Fixes every confirmed backend finding from the v0.3.0 pre-ship review.

Identity (the two that corrupt accounts):
- nostr_register creates a row with pubkey_hash NULL, so that wallet's later
  /auth/extension call matched nothing, fell through the rotation path (which no
  client ever satisfies, since nothing sends rotation_signature) and minted a
  SECOND account. A fingerprint match on a NULL-pubkey_hash row now adopts the
  submitted pubkey_hash onto that row, but only when the row's on-file BTC key
  equals the submitted one, so a bare fingerprint (which check_restore discloses,
  and is therefore not a secret) still cannot claim an account.
- The fingerprint-ownership check no longer short-circuits on returning_by_pubkey.
  It was skipped for returning users, so a fingerprint owned by another row got
  backfilled, violated the UNIQUE index, and 500'd that account on every
  subsequent login.
- The rotation proof is verified over a distinct, instance- and target-bound
  message. It shared the login message, so a captured login signature was a
  replayable rotation proof against another instance.

Abuse control:
- The rate limiters key on the real client IP, honouring X-Forwarded-For only
  from a trusted proxy. Behind the TLS terminator every request looked like
  127.0.0.1, so the whole internet shared one bucket and ~3 req/s 429'd every
  wallet unlock globally.
- A PoW solution is single-use instead of replayable for the challenge TTL.
- Delegated /verify is opt-in and rate-limited; it was an unauthenticated
  amplifier into Electrum, LWS and a full-genesis grin scan.
- A grin scan with no start_height gets the same depth policy and PoW price as
  one that passes a height, instead of an unbounded free scan.

Retention and logging:
- The retention sweep is implemented AND scheduled. RETENTION_* was advertised
  and inert, which made the privacy policy's retention promise untrue. Zero
  means keep forever, and each statement is batched so a backlog drains over
  ticks rather than holding row locks.
- login_events.ip_hash is no longer written, and legacy values are scrubbed:
  nothing ever read it, so it was retention without a purpose.
- Tracing spans log user_id only, not the raw npub and username.
- secret-scan.sh matches the coverage its comment claims.

Also fixes the test suite, which did not compile on this commit: NewSession
gained an id in c64cd9b and two call sites were never updated. mint_session now
binds one session_id for both the token and the row, since a different id there
would silently recreate the bug c64cd9b fixed.
2026-08-13 10:31:02 -04:00

54 lines
2.4 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# secret-scan: block private keys and seed material from entering the tree.
#
# Public-safe by design: it matches on secret *shapes* (key blocks, mnemonic
# markers), never on project-internal names. gitleaks runs alongside it in CI
# for broad token/credential patterns.
#
# ./tools/secret-scan.sh
#
set -uo pipefail
cd "$(dirname "$0")/.."
if git rev-parse --is-inside-work-tree >/dev/null 2>&1 && [ -n "$(git ls-files)" ]; then
mapfile -t FILES < <(git ls-files)
else
mapfile -t FILES < <(find . -type f -not -path './.git/*' -not -path './target/*' -printf '%P\n')
fi
# Secret shapes (not names), covering the classes this backend actually handles:
# - PEM/OpenSSH/PGP private-key blocks
# - seed-phrase markers (mnemonic / seed phrase), any capitalisation
# - nsec1... Nostr SECRET keys (bech32)
# - a secret-shaped env assignment given a 32+ hex-char value, i.e. an
# `openssl rand -hex 32` value pasted in cleartext. The name alternation is
# spelled out so it really does cover every secret this backend takes:
# *_PEPPER, *_SALT, *_SECRET (incl. *_INTEGRITY_SECRET), *_PASSWORD, *_TOKEN,
# *_API_KEY, *_ADMIN_KEY, *_HMAC_KEY, *_PRIVATE_KEY, *_SIGNING_KEY. A bare
# *KEY* is NOT in the list on purpose: it would fire on ADMIN_PUBKEYS and
# friends, whose values are public.
#
# NOT covered, and left to gitleaks rather than implied here: secret VALUES that
# are not hex (base64 or random alphanumeric API keys, real passwords). Catching
# those needs entropy heuristics, which in this tree would fire on every address,
# hash and test vector and train people to ignore the scanner.
PATTERNS='(-----BEGIN ([A-Z ]+ )?PRIVATE KEY-----|-----BEGIN PGP PRIVATE KEY|BEGIN OPENSSH PRIVATE KEY|\b([Mm]nemonic|MNEMONIC|[Ss]eed[_ ]?[Pp]hrase|SEED[_ ]?PHRASE)\b\s*[:=]|nsec1[0-9a-z]{20,}|\b[A-Z_]*(PEPPER|SALT|SECRET|PASSWORD|TOKEN|API_?KEY|ADMIN_KEY|HMAC_KEY|PRIVATE_KEY|SIGNING_KEY)[A-Z_]*\s*=\s*[0-9a-fA-F]{32,})'
fail=0
for f in "${FILES[@]}"; do
[ -f "$f" ] || continue
case "$f" in tools/secret-scan.sh) continue ;; esac
if grep -EnI "$PATTERNS" "$f" >/dev/null 2>&1; then
echo "SECRET-SCAN: possible key/seed material in $f"
grep -EnI "$PATTERNS" "$f" | sed 's/^/ /'
fail=1
fi
done
if [ "$fail" -ne 0 ]; then
echo "SECRET-SCAN FAILED — remove key/seed material before committing."
exit 1
fi
echo "secret-scan OK (${#FILES[@]} files checked)."