Files
coop/docs/nip4e-psstpsst-comparison.md
T

109 lines
5.4 KiB
Markdown

# NIP-4e: psstpsst implementation comparison
A comparison of Coop's NIP-4e implementation (`crates/nip4e`) against the
[psstpsst](https://github.com/CodyTseng/psstpsst) client, which implements the
draft [NIP-4e (per-device keys)](https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md)
with a documented [client profile](https://github.com/CodyTseng/psstpsst/blob/master/docs/protocols/nip4e.md).
## What Coop already covers (well)
| Capability | Coop | psstpsst |
|---|---|---|
| kind `10044` announcement (fetch + publish, `n` tag) | yes | yes |
| kind `4454` request with `P` tag + pairing code | yes | yes |
| kind `4455` transfer, NIP-44 encrypted | yes | yes |
| Requester key derivation & local storage | encrypted-to-self event in local DB | OS vault |
| Announcement-match validation on import | yes (`decrypt_approval`) | yes |
| Delete `4454` + consumed `4455` after success (draft step 12) | yes — psstpsst does *not* do this | no |
| Backup/export of encryption key (nsec) | yes, incl. wasm path | no |
| Requester identity shown to approver (avatar + name) | yes | no (code only) |
| Account-switch state reset | basic | stronger |
## Gaps
### A. Security-critical
1. **Long-lived client key instead of ephemeral per-exchange keys.**
psstpsst generates a throwaway client keypair for every `4454`/`4455`
exchange and never persists it: a `4455` lives on relays forever, so reusing
one client key means a single leak decrypts every key transfer ever addressed
to it. Coop's `get_or_init_app_keys` persists one app key in `credentials`
and reuses it forever.
2. **The `4455` is encrypted with the long-lived encryption key, not a fresh
sender keypair.** The draft (step 10) has the responder generate a fresh
keypair `c` and encrypt to `ecdh(c, T)`, with `P` carrying `C`. psstpsst
follows this. Coop encrypts with the encryption key itself and its `P` tag
carries `E`, so anyone holding the encryption private key — today or after a
future rotation/compromise — can decrypt all historical `4455`s.
3. **No durable processed-request log.** Coop's dedup is an in-memory
`HashSet` plus a single `pending_request: bool`. psstpsst persists every
handled `4454` id so approvals don't re-fire after restart and a device can
recognize its own requests even after its client key is gone.
4. **Announcement-match is skipped when the local DB has no announcement.**
`decrypt_approval` silently accepts a transfer when the local DB query
returns nothing. psstpsst keeps waiting until the transfer matches a known
announcement; the match is the replay-safety mechanism.
5. **No cross-device resolution detection.** psstpsst lets a second existing
device see a `4455` already addressed to the requester's client key and
withdraw its redundant approval, avoiding publishing the secret twice.
Coop's `pending_request` bool means two devices can both approve.
### B. Protocol/interop
6. **Missing `4454` tags psstpsst sends:** `relay` hints (the responder replies
to *shared* relays; without them the `4455` may never reach the requester),
optional `n` (the exact requested key), and the `pubkey` compatibility
alias (psstpsst accepts both `P` and `pubkey`; Coop reads only `P`).
7. **Missing identity `p` tag on `4455`.** psstpsst emits `p` = identity plus
`p` = requester client key; Coop has only the client-key `p`.
8. **No peer announcement caching/freshness model.** psstpsst caches per-peer
announcements in SQLite with tie-break rules (newer `created_at`, then
lexicographically smaller id), a one-day TTL freshness check, and relay
resolution (peer's write + capped DM relays, fallback to discovery). Coop
fetches peer `10044` via a plain subscription in `crates/chat`.
### C. Features
9. **Key rotation.** psstpsst supports scheduled automatic rotation
(1-90 days, default 30, off by default) with single-flight rotation,
startup due-checks, old keys retained in a bounded list for the transition,
and send-readiness gating. Coop has no rotation path.
10. **Resync gating.** psstpsst drops into a full-screen gate that blocks
messaging until a valid key arrives after observing a key change. Coop
re-requests on mismatch but doesn't gate messaging meanwhile.
### D. Engineering
11. **No tests** for `crates/nip4e`. psstpsst has dedicated suites for the
transfer flow, watcher, session, store, and approval sheet.
12. **No client profile doc** pinning down tag MUSTs, code format, and
compatibility. Coop's deviations (#2, #7) are implicit.
## Status
Section A (items 1-5) is implemented in `crates/nip4e/src/lib.rs` and
`crates/state/src/credentials.rs`:
- Ephemeral per-request client key, held in memory for the lifetime of one
request (retries reuse it so an open approval sheet keeps targeting a key the
requester still listens for), cleared after import or account switch.
- `4455` responses encrypt to a fresh one-time responder keypair carried in the
`P` tag, matching the draft and psstpsst.
- Bounded, persisted log of handled `4454` ids; duplicate requests (same client
key) are recorded without re-prompting, replaced requests (different client
key) withdraw the stale sheet.
- `decrypt_approval` falls back to a live relay query when the local database
has no announcement and rejects when none is available.
- An open approval is withdrawn when a `4455` addressed to the same client key
arrives (another device already answered).
Items 6-12 remain open.