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 inc64cd9band 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 bugc64cd9bfixed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2.8 KiB
Privacy Policy
The canonical Smirk privacy policy lives at https://smirk.cash/privacy and describes the data practices of the instance operated by Such Software LLC (api.smirk.cash).
For operators who self-host
This backend is open source and self-hostable. If you run your own instance, you are an independent data controller for it and the Such Software policy does not speak for you. What your instance receives from wallets that connect to it:
- Monero/Wownero view keys (to scan for users' incoming funds; a view key cannot spend) and their scan/registration state.
- Public addresses for BTC/LTC balance lookups and signed transactions to broadcast. Recipient address and amount are not sent on broadcast.
- A one-way seed fingerprint (SHA-256) as a non-reversible wallet identifier.
- If enabled: the username to npub mapping published in your NIP-05 directory, and, for the relay, encrypted message envelopes (ciphertext only).
- IP addresses never in raw form. The per-IP rate limiter keeps its counters
in memory, and the only IP-derived value written to disk is a salted one-way
hash on
restore_attempts, which the seed-restore abuse limiter reads back. Login events record no IP at all, and the raw-IP columns that older schemas carried onsessionsandaudit_logswere dropped in migration20260722000001_privacy_drop_pii. One exception, and it is about you rather than your users: the tamper-evident operator audit chain (admin_audit_logs) records the IP of privileged admin actions.
Private keys and seed phrases are never transmitted to the backend.
Retention
Retention is enforced by the backend, not merely advertised. A background sweep
deletes login_events older than RETENTION_LOGIN_EVENTS_DAYS and audit_logs
older than RETENTION_AUDIT_DAYS, in bounded batches so a long-running instance
drains its backlog without stalling writers. Setting either knob to 0 means
keep indefinitely, so an unset value never destroys data by surprise.
The admin_audit_logs chain is deliberately exempt: it is hash-chained and
MAC'd, so a deleted row reads as tampering to the verifier. Prune it on purpose
if your policy calls for it.
Self-service erasure (ERASURE_ENABLED) is separate and user-initiated: a
confirmed request deletes the account and its owned rows after a grace window,
purging or anonymizing that user's login events per ERASURE_PURGE_LOGIN_EVENTS.
If you offer your instance to others, publish your own privacy policy describing
how you handle this data, your retention, and your jurisdiction. The knobs that
affect retention and data exposure (login-event and audit retention, erasure,
relay retention, landing-page exposure) are documented in
docs/operations/CONSOLE.md and the root .env.example.