Phase 3 is implemented in the fork on claude/jvm-target-actuals (ce49657). The security analysis in the plan held up; three practical constraints around it did not appear until the code was written. **A passphrase-derived KEK is not a drop-in.** The plan treated the choice between a passphrase, an OS keychain and a key file as the whole decision. But keyStoreEncryption(keyName, plainText) takes no context and no secret -- on android the OS holds the key, so none is needed -- which means any passphrase scheme needs an out-of-band unlock the expect cannot express. That is a change to application startup, not just to the actual, so it is now called out against phase 5: the desktop entry point has to prompt and unlock before the wallet starts. **The iv must be 16 bytes.** EncryptedSeed.V2.serialize in commonMain throws on anything else, which rules out a conventional 96-bit GCM nonce -- worth knowing before designing around one. It turns out to help: with randomly generated nonces the risk is a repeat under one key, and 128 bits makes that vanishingly unlikely where 96 merely makes it unlikely. **Argon2id costs a dependency.** The jdk has PBKDF2 and no memory-hard KDF at all, so it means bouncycastle. Recorded with the reason to pay it: if the build is dev-only because it lacks hardware backing, weakening the KDF too gets the trade backwards. Also recorded: wrap a per-name data key under the KEK rather than encrypting the seed with it directly, so a passphrase change rewraps 32 bytes; throw java.security.KeyStoreException when locked, since the graceful* wrappers already map it to DecryptSeedResult.Failure.KeyStoreFailure; and a verification section naming the properties that fail quietly, plus the two limits worth writing down rather than fixing -- the first unlock on a new store accepts any passphrase, and zeroing the key is best effort. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
mantra docs
Notes on the parts of this app whose behaviour is not recoverable by reading the code alone — where the reasoning lives in a protocol, a failure mode that is silent, or a decision that looked arbitrary and was not.
| document | covers |
|---|---|
| shared-key-ceremony.md | ChillDKG over NIP-17: the rounds, the approval gates, the chat transcript, participant ordering |
| shared-key-derivation.md | deriving further keys from the group's threshold key with FROST tweaks — why not BIP32, why no chain code, and the one rule that must not be broken |
| marmot-membership.md | how members join an MLS group, and the epoch race that makes a missing member look like a successful invite |
| mls-skipped-keys.md | why a group event that arrives a moment late is dropped for good, which flows trigger it, the quartz fix, and the partial mitigation in this app |
| jvm-target.md | what desktop support would cost, phased — why the native chain is already done, why an empty source set in our phoenix fork is the real blocker, and why DAO tests do not need any of it |
Start with the ceremony if you are new to this area; the other two both assume it. Read the skipped-keys note before debugging any "the other device never got it" report — it is silent, and it looks like every other kind of delivery failure.