track key-sync request state to prevent duplicates
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user