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).
81 lines
4.2 KiB
Markdown
81 lines
4.2 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` — `expect 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
|
|
|
|
```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.
|