# SNIN Mail — reproducible crypto fixture (NIP-44 / NIP-59)

**What this is.** A machine-readable fixture for the cryptographic core of
[SNIN Mail / nostr-mail-bridge](https://github.com/konantgit-sys/snin-mail-nostr):
NIP-44 v2 encryption and NIP-59 gift wrap. Every case is
`input -> expected -> actual -> verdict`. Nothing is asserted without the bytes
to back it: the artifact is generated by running the code, not written by hand.

**Artifact.** `fixture.json` — 130 cases, 19 areas.
SHA-256 of `fixture.json`:
`6110a576d2a1a3eba054d53245c23d5406dadd5962303fd214c80e47bacac610`

**Reproducibility.** The generator uses only deterministic inputs
(spec vectors, fixed nonces, throwaway keys derived from strings), so two runs
produce a byte-identical file. Verified: two consecutive runs give the same
SHA-256 above. NIP-59 legitimately generates a fresh ephemeral key and a tweaked
timestamp per message, so those values are **not** written into the artifact —
only the properties that must hold.

## How to replay

```bash
git clone https://github.com/konantgit-sys/snin-mail-nostr
cd snin-mail-nostr
pip3 install --break-system-packages -r requirements.txt
python3 tools/make_fixture.py > fixture.json
sha256sum fixture.json     # must equal 6110a576d2a1a3eba054d53245c23d5406dadd5962303fd214c80e47bacac610
```

No network access is needed. No secrets are involved: the private keys in the
fixture are NIP-44 spec vectors and throwaway keys derived from public strings.
No mailbox credentials, no user data, no relay traffic.

## Coverage

| Area | Cases | What it pins down |
|---|---|---|
| `nip44.conversation_key` | 35 | ECDH → conversation key, incl. boundary keys (sec1 = n-2, sec1 == pub2) |
| `nip44.message_keys` | 32 | HKDF split into chacha key/nonce + hmac key |
| `nip44.encrypt_payload` | 10 | payload is byte-identical to the official vector for a given nonce |
| `nip44.decrypt_payload` | 10 | decrypt returns the exact plaintext |
| `nip44.long_message` | 3 | 65535-byte messages: SHA-256 of plaintext and payload |
| `nip44.invalid_key` | 8 | invalid pubkeys (order 3/13/3319 on twist, no sqrt, sec1 ≥ n) rejected |
| `nip44.invalid_payload` | 12 | wrong version, broken base64, bad MAC, bad padding, bad lengths rejected |
| `nip44.length_limit` | 4 | plaintext lengths 0 / 65536 / 100000 / 10000000 rejected |
| `nip44.edge_roundtrip` | 5 | UTF-8 edges: 1 byte, Cyrillic, emoji, RTL, mixed |
| `nip44.tamper` | 1 | one flipped MAC byte → decryption fails, no plaintext leak |
| `nip59.*` | 8 | wrap kind 1059, signature valid, roundtrip, wrong recipient fails, sender not disclosed, timestamp handling |

## Two measured facts (recorded, not hidden)

Marked `OBSERVED` in the artifact — they are deviations measured against the
NIP-59 recommendation, not test failures:

1. **Tweak window.** NIP-59 says the gift wrap timestamp *SHOULD* be randomized
   to within **two days** of the rumor's timestamp. This implementation uses
   `±60 s` (`implemented in src/mailbridge/nip59.py::wrap`). Narrower window =
   weaker resistance to time correlation.
2. **Envelope vs rumor time.** `wrap()` called without an explicit `created_at`
   stamps the envelope with the current wall clock and does not inherit
   `rumor.created_at`. A re-sent or imported old message therefore gets a
   present-day envelope timestamp. In the normal send path the rumor is created
   in the same call, so the difference is ≤ 60 s there — the observation applies
   to the raw API and to re-sends.

## Boundaries

- No claim of security auditing: this pins down *behaviour*, not absence of side
  channels, and it does not cover relay-level metadata or IMAP credential storage.
- `verified_artifact` stays a raw locator here: we present the bytes and the
  replay command, and leave the verdict to an independent verifier.
- The fixture is read-only evidence. Nothing in the running mail service was
  modified to produce it.
