mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-30 07:37:38 +00:00
quic-kmp's crypto-kmp release ships as 1.0.1, not 2.0.0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017FiX3CsiEsvjJ7zHNHD3Kk
245 lines
14 KiB
Markdown
245 lines
14 KiB
Markdown
# A `quartz-crypto` module that Quartz and quic-kmp share
|
|
|
|
Status: **superseded by a separate repository** (2026-10-20). In review the module became its own
|
|
repo, [vitorpamplona/crypto-kmp](https://github.com/vitorpamplona/crypto-kmp), and the primitives
|
|
were renamed to `com.vitorpamplona.crypto.*` (`sha2`, `digest`, `mac`, `kdf`, `aes`, `chacha`,
|
|
`curve25519`, `random`), with every import in quartz, the apps and quic-kmp rewritten instead of
|
|
keeping the old package names. That reverses D1 and D3 below. Phases 1 and 3 are done that way:
|
|
crypto-kmp 1.0.0 holds the code, quartz takes it as `api(libs.crypto)`, and quic-kmp 1.0.1 depends
|
|
on it instead of quartz. `Hex` and `Log` stayed in quartz; NIP-44's `fastExpand`/`MessageKey`
|
|
stayed in quartz as `nip44Encryption/crypto/Nip44MessageKey.kt`. Phase 2 (a public API shaped for
|
|
quic's hot path) and Phase 4 remain open, now in crypto-kmp. The text below is the original plan.
|
|
|
|
Follows [vitorpamplona/amethyst#4586](https://github.com/vitorpamplona/amethyst/pull/4586), which
|
|
replaced Quartz's third-party native crypto with in-repo code.
|
|
|
|
## Why
|
|
|
|
quic-kmp ([vitorpamplona/quic-kmp](https://github.com/vitorpamplona/quic-kmp)) declares
|
|
`api(libs.quartz)` to reach 11 symbols:
|
|
|
|
| Symbol | Where it lives in Quartz |
|
|
| --- | --- |
|
|
| `X25519`, `X25519KeyPair` | `mls.crypto` |
|
|
| `ChaCha20Core`, `ChaCha20Poly1305`, `Hkdf` | `nip44Encryption.crypto` |
|
|
| `AESGCM`, `MacInstance`, `sha256`, `RandomInstance` | `utils.ciphers`, `utils.mac`, `utils.sha256`, `utils` |
|
|
| `Log`, `TimeUtils` | `utils` (not crypto) |
|
|
|
|
`Log` and `TimeUtils` are habits from the days quic lived in Amethyst, not needs: 4 debug log
|
|
calls and 2 default clocks. quic-kmp drops both on its own (see Phase 3), so only the 9 crypto
|
|
symbols are left for this module to serve.
|
|
|
|
Three problems follow:
|
|
|
|
1. **Everything else in Quartz comes with it.** Every quic user gets Quartz's other `api`
|
|
dependencies on their classpath: `androidx.sqlite`, `negentropy-kmp`, `secp256k1-kmp` with its
|
|
JNI builds, and Jackson on JVM/Android. A WebTransport client has no use for any of them.
|
|
2. **Generic primitives sit under Nostr names.** ChaCha20 lives in `nip44Encryption`, X25519 in
|
|
`mls`. quic imports from a NIP-44 package to run TLS.
|
|
3. **The best code is unreachable.** #4586 left Quartz with its own SHA-1/SHA-256/SHA-512
|
|
(with state copy), HMAC with cached key states, and constant-time AES, AES-GCM and AES-CBC.
|
|
All of it is `internal`. quic's native placeholders (`quic/src/nativeCheck`) need exactly
|
|
these: a TLS transcript hash that can be snapshotted (`ShaDigest.copyFrom`) and a one-block
|
|
AES for header protection. That is the groundwork for audio rooms on iOS
|
|
(`commons/plans/2026-10-09-ios-app.md`, B11).
|
|
|
|
## What moves
|
|
|
|
60 main-source files: 34 in `commonMain`, 12 in `jvmAndroid`, 5 in `appleMain`, 4 in
|
|
`nativeMain`, 5 in `linuxMain`. Their tests move with them.
|
|
|
|
| Group | Files (packages unchanged) |
|
|
| --- | --- |
|
|
| SHA-1/2 | `utils/sha2/` (`ShaDigest`, `Sha256Compression`), `utils/sha256/` (`sha256`, `sha256Into`, the JVM `Sha256Pool`/`Sha256Hasher`/`CountingInputStream`, Apple and Linux actuals), `nip13Pow/miner/Sha256Midstate` |
|
|
| Other digests | `utils/diggest/` (`DigestInstance` + actuals, `StreamingDigest`), `nip03Timestamp/ots/crypto/` (`RIPEMD160Digest`, `KeccakDigest`, `GeneralDigest` and its BouncyCastle-style interfaces, `Pack`) |
|
|
| MAC and KDF | `utils/mac/` (`MacInstance` + actuals, `Hmac`), `nip44Encryption/crypto/Hkdf` |
|
|
| Ciphers | `utils/ciphers/` (`AESCBC`, `AESGCM`, `NostrCipher` + actuals), `utils/ciphers/aes/`, `nip44Encryption/crypto/` (`ChaCha20`, `ChaCha20Core`, `Poly1305`, `ChaCha20Poly1305`, `XChaCha20Poly1305`) |
|
|
| Curves | `mls/crypto/` `X25519`, `Curve25519Field`, `Ed25519` + actuals |
|
|
| Support | `utils/` `RandomInstance`, `SecureRandom` + actuals, `ConstantTime`, `Hex` |
|
|
|
|
The support files move because the crypto files need them: `Hkdf` compares MACs with
|
|
`equalsConstantTime`, anything random goes through `RandomInstance`, and hex is the byte codec
|
|
error messages print with. None of them imports anything else from Quartz.
|
|
|
|
**No logging in the module.** Six moving files log today:
|
|
- `decryptOrNull` warns on every failed decryption: `AESCBC` (JVM/Android, Apple, Linux) and
|
|
`AESGCM` (JVM/Android, native).
|
|
- The JVM `Sha256Pool` warns when it runs low.
|
|
|
|
Phase 1 removes these six calls instead of moving `Log`, and the module ends up depending on
|
|
nothing but the Kotlin stdlib:
|
|
- A library primitive can't tell an expected `null` from an alarming one. Commons'
|
|
`EncryptedBlobInterceptor` returns `null` on purpose for some blobs and retries with a UTF-8
|
|
nonce, so that warning is noise.
|
|
- The other two callers, Concord's `ImagePointer` and `CyberspaceBagEvent`, can log their own
|
|
`null` where it means something.
|
|
- The pool warning has nothing to act on: `take()` blocks until a digest comes back.
|
|
|
|
**What stays in Quartz:**
|
|
- **secp256k1/Schnorr.** It is Nostr's curve and carries the JNI builds; moving it would bring
|
|
those back to quic.
|
|
- **`Hpke` and `MlsCryptoProvider`.** They depend on the MLS TLS codec (`mls.codec`).
|
|
- **The protocol compositions:** NIP-04, NIP-44 v2, NIP-49, gift wraps, Cashu BDHKE, the
|
|
OpenTimestamps ops.
|
|
- **`Log`, `LogLevel`, `LogSink`** (414 files outside Quartz import `Log`) and **`TimeUtils`**.
|
|
Neither is crypto, and after Phase 3 quic uses neither.
|
|
|
|
**Boundary check (run on 2026-10-20):** the moving files import exactly one thing from outside the set,
|
|
`nip01Core.core.toHexKey`, used in `Hkdf` (an error message) and `AESGCM.jvmAndroid`. Both
|
|
become `Hex.encode`, which moves. One `internal` symbol crosses the line: `Sha256Compression`,
|
|
used by `Sha256Midstate`. Moving the miner's midstate too keeps it `internal`. It's plain
|
|
SHA-256 resumed from a saved state, used by `PoWMiner`, `PoWHashMode` and commons'
|
|
`PoWEstimator`, all through its public API.
|
|
|
|
## Decisions
|
|
|
|
**D1. A module in this repo, not a new repo.**
|
|
- Quartz writes most of this code, and a primitive and its first use should land in one commit.
|
|
- It ships on Quartz's release train at the same version, so there is no third version to keep
|
|
aligned.
|
|
- It fits the existing rule: a module for code with no Nostr in it (`nestsClient`, `marmotQuic`).
|
|
|
|
**D2. Name.** Gradle `:quartzCrypto` (the repo's camelCase module style), published as
|
|
`com.vitorpamplona.quartz:quartz-crypto` at `libs.versions.app`, beside `quartz`.
|
|
|
|
**D3. Keep every package name.** This is the `commonsUI` precedent: "the split is a module
|
|
boundary, not a package rename".
|
|
- Outside Quartz, 37 files import `utils.sha256`, 33 `RandomInstance` and 22 `utils.ciphers`.
|
|
None of them changes.
|
|
- Quartz declares `api(project(":quartzCrypto"))`, so a consumer that depends on `quartz`
|
|
compiles unchanged.
|
|
- The cost is a package split across two jars. Kotlin, Android and R8 don't care. JPMS would,
|
|
but Quartz ships no `module-info`.
|
|
- Moving the misplaced packages to `com.vitorpamplona.quartz.crypto` is a separate, optional
|
|
step. Typealiases can bridge classes but not top-level functions such as `sha256`, so it would
|
|
be a breaking change for Quartz users.
|
|
|
|
**D4. Public API, platform-best by default.** Phase 2 promotes what quic needs. On JVM and
|
|
Android, quic gets its speed from JCA (cached `Cipher`, range `doFinal`), and a pure-Kotlin
|
|
public class would be a step backwards there. So each new public type is an `expect` with JCA
|
|
actuals on `jvmAndroid` and the Kotlin code on native, the shape quic's own
|
|
`PlatformCrypto.kt` already has:
|
|
- **Digest with snapshot:** an incremental SHA-256/SHA-384/SHA-512 with `copy()`. JVM uses
|
|
`MessageDigest.clone()`; native uses `ShaDigest.copyFrom`. SHA-384 is new: TLS 1.3's
|
|
`TLS_AES_256_GCM_SHA384` needs it, and quic defines but rejects that suite today.
|
|
- **One-block AES** encrypt, for QUIC header protection. JVM: `AES/ECB/NoPadding`. Native:
|
|
`AesBitsliced`.
|
|
- **AES-GCM and ChaCha20-Poly1305 bound to a key**, with range input and in-place output
|
|
(`sealInto`/`openInto`) like quic's `Aead`. JVM: cached JCA `Cipher`. Native: `AesGcmKey`
|
|
and the existing ChaCha20-Poly1305.
|
|
- `X25519`, `Hkdf`, `ChaCha20Core` and `RandomInstance` are already public and stay as they are.
|
|
|
|
`AesBitsliced`, `Ghash` and `Hmac` stay `internal`: they are implementation, not API.
|
|
|
|
**D5. No binary-compatibility tooling yet.** Quartz has never promised a stable API, and its
|
|
version tracks the app. Phase 2's public types are the first promise worth guarding, so add
|
|
Kotlin's binary-compatibility-validator to `:quartzCrypto` alone when they land. It is a new
|
|
dependency, so check its license against its actual `LICENSE` file before wiring it in, as
|
|
`AGENTS.md` requires.
|
|
|
|
## Phases
|
|
|
|
### Phase 1: create the module and move the files (no API change)
|
|
|
|
1. Create `quartzCrypto/build.gradle.kts`.
|
|
- Copy `quartz/build.gradle.kts` for the targets: JVM, Android, `iosArm64`,
|
|
`iosSimulatorArm64`, `macosArm64`, `linuxX64`.
|
|
- Copy the source-set tree: `jvmAndroid`; `nativeMain` above `appleMain` (iOS and macOS) and
|
|
`linuxMain`; the test sets to match.
|
|
- Copy the Android namespace, publishing and signing blocks.
|
|
- Its dependencies are the Kotlin stdlib alone. Check that nothing else is needed.
|
|
2. Add `include(":quartzCrypto")` to `settings.gradle.kts`, and
|
|
`api(project(":quartzCrypto"))` to Quartz's `commonMain`.
|
|
3. `git mv` the files listed above, keeping history, then their tests. The tests that move:
|
|
- `utils/sha2`, `utils/mac`, `utils/ciphers/aes`, `utils/sha256`;
|
|
- `nativeTest` `DigestInstanceNativeTest`;
|
|
- `jvmTest` `ShaDigestJcaTest` and `AesJcaTest`;
|
|
- `Sha256MidstateTest`;
|
|
- the NIP-44 primitive tests (`ChaCha20CoreTest`, `ChaCha20Poly1305Test`, `XChaCha20Test`,
|
|
`HkdfText`);
|
|
- `jvmAndroidTest` `Ed25519Test`.
|
|
|
|
Tests of compositions (NIP-04/44 envelopes, MLS) stay in Quartz.
|
|
4. Replace the two `toHexKey()` calls with `Hex.encode`, and remove the six log calls (see "No
|
|
logging in the module"). The only behaviour change: failed decryptions in `decryptOrNull`
|
|
no longer log a warning.
|
|
5. Build wiring:
|
|
- apply `gradle/kmp-purity.gradle.kts`;
|
|
- add `:quartzCrypto` to the `lint` job's purity and metadata compiles;
|
|
- add its `jvmTest` to the JVM test jobs, to `test-quartz-linux-native` and to
|
|
`test-quartz-ios`;
|
|
- add it to `.git-hooks/pre-push` (keep the hook and `build.yml` in sync, as the hook says);
|
|
- add it to the root `test` alias.
|
|
6. R8: `quartz/consumer-rules.pro` names none of the moved classes today. Recheck it and
|
|
`amethyst/proguard-rules.pro` at move time, and give the module its own consumer rules if
|
|
one turns out to be needed.
|
|
7. Docs:
|
|
- `quartz/CLIENT.md` and `quartz/agent-skill/quartz/`: name the new artifact;
|
|
- `AGENTS.md`: module list, architecture tree, the "sharing philosophy" line for crypto;
|
|
- the `kotlin-multiplatform` skill, if it lists modules.
|
|
|
|
**Done when:**
|
|
- every module builds with no source change outside Quartz;
|
|
- the moved tests pass in `:quartzCrypto` on JVM, Linux native and iOS;
|
|
- `publishToMavenLocal` produces both artifacts;
|
|
- Quartz's POM lists `quartz-crypto` as a compile dependency.
|
|
|
|
### Phase 2: the public API quic needs
|
|
|
|
Add D4's types to `:quartzCrypto`, each with:
|
|
- JCA and Kotlin implementations;
|
|
- the existing OpenSSL and NIST vectors run through every platform;
|
|
- a JVM test against JCA;
|
|
- a Linux native run.
|
|
|
|
Then add the binary-compatibility validator (D5). Quartz's own callers don't change.
|
|
|
|
### Phase 3: quic-kmp 2.0.0 on `quartz-crypto`
|
|
|
|
1. **Done ahead of this plan,** on quic-kmp's `claude/compassionate-wozniak-743sh0` branch
|
|
(`43a5cb5`, not yet merged or released): quic stops using Quartz's `TimeUtils` and `Log`.
|
|
- The two default clocks read `kotlin.time.Clock.System`, which is stable since Kotlin 2.3.
|
|
- The four debug logs go through quic's own `QuicLog` hook, which logs nothing until a sink
|
|
is set.
|
|
- Amethyst must point `QuicLog.sink` at Quartz's `Log` when it bumps `quicKmp`. Otherwise
|
|
quic's debug lines disappear from its logs.
|
|
2. Then:
|
|
- depend on `com.vitorpamplona.quartz:quartz-crypto`, replacing `quartz`;
|
|
- move `crypto/PlatformCrypto.kt` and the AEADs onto Phase 2's types where they are at
|
|
least as fast. Keep quic's `Aead` interface, which carries the RFC 9001 limits.
|
|
3. Version 2.0.0. Two caveats:
|
|
- **`X25519KeyPair` is no change.** It's in quic's public API (`TlsClient`), but it keeps its
|
|
fully qualified name.
|
|
- **Dropping Quartz is a breaking change anyway.** Anyone relying on quic to bring in Quartz
|
|
must now add it themselves. Amethyst's `nestsClient` and `marmotQuic` already depend on
|
|
Quartz directly.
|
|
4. Release order:
|
|
1. `quartz` and `quartz-crypto` publish together.
|
|
2. quic-kmp 2.0.0 publishes, pinned to that version (it pins Quartz `1.19.0` today).
|
|
3. Amethyst bumps `quicKmp`.
|
|
5. With `-PquicKmpDir=../quic-kmp`, the included quic build must resolve `quartz-crypto` to this
|
|
checkout's `:quartzCrypto`, the way it resolves `quartz` to `:quartz` today. Check that the
|
|
coordinates match, so Gradle's composite build swaps it in without an explicit substitute.
|
|
|
|
### Phase 4 (own plan): quic on native
|
|
|
|
- Real native actuals:
|
|
- the transcript hash on Phase 2's digest snapshot;
|
|
- header protection on the one-block AES;
|
|
- the AEADs on the bound-key types;
|
|
- a UDP socket on POSIX or Network.framework.
|
|
- Then an iOS target for quic and `nestsClient`.
|
|
|
|
This is item B11 of the iOS plan. It needs its own plan for the socket and audio work.
|
|
|
|
## Open questions for review
|
|
|
|
Settled in review: the module stays crypto-only (`Log` and `TimeUtils` remain in Quartz, and
|
|
quic stops using them), which also settles the name: `quartzCrypto` / `quartz-crypto`.
|
|
|
|
1. **secp256k1.** Keeping it in Quartz (recommended) means `quartz-crypto` never needs JNI. Is
|
|
there a consumer that wants Schnorr without the rest of Quartz?
|
|
2. **Package rename.** Leave the misplaced packages (`nip44Encryption.crypto`, `mls.crypto`)
|
|
as they are for good, or plan the breaking rename to `com.vitorpamplona.quartz.crypto` for a
|
|
later major release?
|