Files
amethyst/.claude/skills/nostr-expert/references/crypto-and-encryption.md
T
Claude 60edd473c7 Refreshes .claude/ skill library: fixes stale refs, adds 4 new skills
- Updates CLAUDE.md tech stack to current versions (Compose 1.10.3, Kotlin 2.3.20).
- Reframes kotlin-multiplatform iOS as mature; adds secp256k1-kmp 0.23.0 references.
- Updates desktop-expert Main.kt references (code grew from ~270 to 1341 lines and
  NavigationRail moved to ui/deck/SinglePaneLayout.kt); replaces obsolete
  "hardcoded ctrl = true" anti-pattern note with accurate isMacOS branching.
- Removes compose-desktop.md (superseded by desktop-expert/).
- Adds nostr-expert references: nip19-bech32, event-factory, crypto-and-encryption,
  large-cache. Adds kotlin-expert/common-utilities, compose-expert/rich-text-parsing,
  android-expert/image-loading.
- New skills: account-state (Account + LocalCache), relay-client (subscriptions,
  filter assemblers, preloaders), feed-patterns (FeedFilter + FeedViewModel family),
  auth-signers (NostrSigner across internal / NIP-46 / NIP-55).
2026-04-21 21:00:45 +00:00

4.2 KiB

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.ktEvent.verify() / Event.hasValidSignature() extensions.

secp256k1 abstraction (quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/)

  • Secp256k1Instance.ktexpect object with signSchnorr, verifySchnorr, pubKey(seckey), sharedSecret.
  • Secp256k1InstanceC.kt — C-based actual using secp256k1 JNI (Android/JVM).
  • Secp256k1InstanceKotlin.kt — pure-Kotlin actual (iOS via native, etc.).
  • Android actual: secp256k1-kmp-jni-android (0.23.0). JVM actual: secp256k1-kmp-jni-jvm.

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

// 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

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

// 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.