Files
amethyst/.claude/skills/nostr-expert/references/crypto-and-encryption.md
Claude 19fcd33da7 refactor(quartz): one native clock, copy-free Linux sha256Into, doc fixes
Follow-ups from auditing the expect/actual consolidation:

- currentTimeSeconds/currentTimeMillis: Apple (NSDate) and Linux
  (clock_gettime) kept two native clocks. POSIX clock_gettime exists on
  Darwin too, so one nativeMain actual now serves every native target,
  with exact integer millis instead of a Double round-trip and no ObjC
  object per call. clockid_t is Int on Linux and UInt on Darwin, hence
  convert(). New PlatformClockTest checks the clock on every target.
- Linux sha256: resolve the hasher once instead of per call, and have
  sha256Into hash data[0, len) straight into `out` (update + hashIntoByteArray)
  instead of slicing the input and copying a temporary digest.
- Rename the catalog alias dev-whyoleg-cryptography-provider-apple-optimal
  to ...-provider-optimal: it is the platform-neutral provider, and it now
  sits in nativeMain where Linux uses it too.
- mls/README invariant grep now covers nativeMain, where the native
  Ed25519/X25519 moved.
- kotlin-multiplatform and nostr-expert skill references no longer
  describe Secp256k1Instance as expect/actual.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KEc94XnZosJhHe96jKfovo
2026-10-06 15:31:08 +00:00

80 lines
4.4 KiB
Markdown

# Crypto & Encryption in Quartz
Event signing, hashing, and NIP-44 payload encryption.
## Layout
### Core crypto (`quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/crypto/`)
- `EventHasher.kt` — canonical JSON serialization + SHA-256 → event id. NIP-01 §1.
- `EventHasherSerializer.kt` — Jackson serializer that emits the exact byte layout NIP-01 hashing requires.
- `KeyPair.kt` — holder for `privateKey: ByteArray` + derived `pubKey: ByteArray`. Generates fresh key pairs via `secureRandom`.
- `Nip01Crypto.kt` — one-stop helper: sign an event, verify a signature, derive pubkey from seckey.
- `EventAssembler.kt` — takes an unsigned template + signer and produces a fully populated `Event`.
- `EventExt.kt` — `Event.verify()` / `Event.hasValidSignature()` extensions.
### secp256k1 abstraction (`quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/`)
- `Secp256k1Instance.kt` — plain common `object` (no expect/actual) wrapping secp256k1-kmp's `Secp256k1.get()`: `signSchnorr`, `verifySchnorr`, `compressedPubKeyFor`, tweaks.
- The platform binding comes from the dependency, not from Quartz code: `secp256k1-kmp-jni-android` on Android, `secp256k1-kmp-jni-jvm` on JVM, the cinterop build on Native.
- Tests/benchmarks pull in `com.vitorpamplona:schnorr256k1-kmp` (libschnorr256k1) for the in-house C JNI baseline used in `Secp256k1CrossValidationTest` and the 3-way benchmarks; production never ships it.
### NIP-44 encryption (`quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip44Encryption/`)
- `Nip44.kt` — dispatcher that handles both v1 (ChaCha20 w/o Poly1305, legacy) and v2 (XChaCha20-Poly1305).
- `Nip44v2.kt` — current spec: HKDF key derivation → XChaCha20-Poly1305 → padded plaintext → Base64.
- `Nip44v1.kt` — legacy path (decrypt-only for backward compat; do not encrypt with v1).
- `crypto/` — `ChaCha20Poly1305`, `HKDF`, `Hmac`, etc. (pure Kotlin, MPP-friendly).
- `SharedKeyCache.kt` — in-process LRU for ECDH shared secrets. Critical for performance in chat/list screens that decrypt many messages with the same counterparty.
- `EncryptedInfoString.kt` — versioned payload envelope that the parser reads to pick v1 vs v2.
## Typical Flows
### Sign an event
```kotlin
// Direct (when you have the privkey in memory)
val signed = Nip01Crypto.sign(unsignedEvent, keyPair.privateKey)
// Via signer (preferred — honors external/remote signers)
val signer: NostrSigner = ... // NostrSignerInternal, Nip46RemoteSigner, NostrSignerExternal
signer.sign(template) { signed -> /* emit signed event */ }
```
Use `NostrSigner` whenever the key might not live in the current process (NIP-46 bunker, NIP-55 Android external signer). See the `auth-signers` skill.
### Verify an event
```kotlin
event.verify() // throws on failure
event.hasValidSignature() // returns Boolean
```
Both recompute `sha256(canonicalJson(event))` and call Schnorr `verifySchnorr(sig, hash, pubKey)`.
### NIP-44 encrypt / decrypt
```kotlin
// Always compute shared secret through the cache — direct ECDH is expensive
val sharedSecret = SharedKeyCache.getOrComputeShared(mySeckey, theirPubkey)
val cipherText = Nip44.encrypt(plaintext, sharedSecret) // v2 by default
val plain = Nip44.decrypt(cipherText, sharedSecret) // dispatches on version byte
```
Callers rarely touch `Nip44v2` directly; go through `Nip44`.
## Gotchas
- **Never log private keys, shared secrets, or raw plaintext.** `KeyPair.privateKey` is a `ByteArray` on purpose so it doesn't get interned as a String.
- **Don't recompute ECDH per message.** `SharedKeyCache` exists because the same counterparty appears in many messages; bypassing the cache produces noticeable UI lag.
- **`EventHasher` ordering is canonical.** Serialize tags / content exactly as `EventHasherSerializer` emits, or ids won't match relays.
- **secp256k1 JNI is platform-specific**: if you add crypto that must run in `commonTest`, wrap it in `expect/actual` or you'll get `UnsatisfiedLinkError` in JVM unit tests.
- **NIP-44 pads messages**. Don't assert exact ciphertext length; assert decrypt round-trips.
## Tests
- `quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/crypto/` — sign/verify/hash round-trips.
- `quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip44Encryption/` — NIP-44 vectors (encryption parity with reference vectors).
- JNI crypto is exercised in `androidUnitTest` / JVM integration tests.