60edd473c7
- 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).
4.2 KiB
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 forprivateKey: ByteArray+ derivedpubKey: ByteArray. Generates fresh key pairs viasecureRandom.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 populatedEvent.EventExt.kt—Event.verify()/Event.hasValidSignature()extensions.
secp256k1 abstraction (quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/)
Secp256k1Instance.kt—expect objectwithsignSchnorr,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.privateKeyis aByteArrayon purpose so it doesn't get interned as a String. - Don't recompute ECDH per message.
SharedKeyCacheexists because the same counterparty appears in many messages; bypassing the cache produces noticeable UI lag. EventHasherordering is canonical. Serialize tags / content exactly asEventHasherSerializeremits, or ids won't match relays.- secp256k1 JNI is platform-specific: if you add crypto that must run in
commonTest, wrap it inexpect/actualor you'll getUnsatisfiedLinkErrorin 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.