@nfinitmonkeys/clan-crypto
v0.2.0
Published
Sovereign encrypted-brain crypto core for Nfinit Monkeys clans — client-held X25519 keys, AES-256-GCM envelope encryption, per-recipient DEK wrapping, human-escrow recovery. Node built-in 'crypto' only; zero third-party deps. The Jungle stores ciphertext
Downloads
406
Maintainers
Readme
@nfinitmonkeys/clan-crypto
Sovereign encrypted-brain crypto core for Nfinit Monkeys clans. Node built-in
crypto only — zero third-party crypto dependencies.
Trust model
- Each clan holds an X25519 encryption keypair — separate from the P-256/ECDSA signing keys used by step-up / Pocket. Never conflate them.
- The clan private key lives client-side only (in the repo, gitignored) and is never sent to or usable by the Jungle server.
- The Jungle stores only: each clan's public key, the ciphertext of private pages, the per-recipient wrapped keys, and grant/revoke audit records. The server can never decrypt. That is the sovereignty guarantee.
Envelope encryption
- Per private page:
DEK = randomBytes(32). Body encrypted withAES-256-GCM(DEK, iv=randomBytes(12)). - The DEK is wrapped to each recipient's X25519 public key via ECDH-ES:
ephemeral X25519 →
diffieHellmanshared secret →HKDF-SHA256KEK (salt = ephemeral SPKI DER, info =clan-brain-dek-wrap-v1‖0x00‖ recipient SPKI DER) →AES-256-GCM. Binding the recipient's static key intoinfo(per RFC 9180 §5.1) prevents unknown-key-share / identity- misbinding: a wrap made for R is undecryptable under any key R'≠R, and therecipientalphaId label can no longer be silently retargeted by an operator. - Owner always reads — the owner's public key is always in the recipient set.
- Grant(alias): owner adds a wrap of the page's DEK to the grantee's key.
- Revoke(alias): rotate the DEK — re-encrypt the body under a fresh DEK and re-wrap for the remaining recipients only. Standard caveat: a party that already cached the plaintext keeps that copy; revocation stops future reads.
- Recovery / escrow (human, never Jungle): wrap the clan private key to the human owner's recovery public key. The Jungle may store this ciphertext blob; it can never open it. Shamir split is a noted future enhancement.
Wire format
EncryptedPage = {
v: 2, // v2 binds recipient SPKI into the wrap KDF info (v1 never shipped data)
alg: "x25519-hkdf-sha256-aes256gcm",
priv: true,
body: { iv, ct, tag },
wraps: [ { recipient, epk, iv, ct, tag } ] // one per reader; owner always included
}All inline binary is base64url (no padding). Public keys are SPKI DER, base64
(matching step_up_devices.public_key_spki).
API
generateClanKeypair(): { publicKeySpkiB64, privateKeyPkcs8B64 } // X25519
publicKeyFromSpkiB64(b64) / privateKeyFromPkcs8B64(b64) // -> KeyObject, validated X25519
publicKeyFromPrivateB64(pkcs8B64): string // derive SPKI from private
encryptPage(body, recipients[, seam]): EncryptedPage // caller MUST include owner
decryptPage(page, myAlphaId, myPrivatePkcs8B64): string
grantAccess(page, ownerAlphaId, ownerPrivB64, grantee[, seam]): EncryptedPage
revokeAccess(page, ownerAlphaId, ownerPrivB64, remaining[][, seam]): EncryptedPage
escrowPrivateKey(clanPrivB64, recoverySpkiB64[, seam]): EscrowBlob
recoverPrivateKey(blob, recoveryPrivB64): string
sealPrivateKeyWithPassphrase(privB64, passphrase[, seam]): string // scrypt N=2^15 + AES-256-GCM
openPrivateKeyWithPassphrase(sealed, passphrase): stringseam is an optional CryptoSeam for deterministic KATs
({ randomBytes?, ephemeral? }); production callers omit it. It is TEST-ONLY
and structurally gated: an injected seam is honored ONLY when
CLAN_CRYPTO_ALLOW_TEST_SEAM=1 is set in the environment, so a stray production
caller that passes a seam gets it silently ignored (secure defaults) rather than
a weakened cipher. Independently, a process-wide (KEK, iv) guard throws on any
AES-GCM nonce reuse, so even a misused seam fails loudly instead of leaking.
Test
npm install && npm test # tsc build + adversarial node runner