This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
mini-kms is an educational single-machine Key Management Service in Java: envelope encryption with rotatable keys served to local processes over sockets. It is heavily commented on purpose — the code is meant to be read to learn how a KMS works. It uses real, sound crypto (Argon2id, AES-256-GCM/AEAD) but is explicitly not production-audited. Keep that intent in mind: clarity and correct security reasoning matter more than features, and comments carry real teaching weight.
Requires JDK 21+ (the Gradle toolchain is pinned to 21; it can auto-download via the foojay resolver).
./gradlew build # compile + run ALL tests (this is the CI check)
./gradlew test # tests only, all modules
./gradlew :core:test # one module
./gradlew :core:test --tests "*LocalKeyringTest" # one test class
./gradlew :core:test --tests "*LocalKeyringTest.rotationKeepsOldCiphertextsDecryptable" # one method
./gradlew :server:installDist :client:installDist # produce runnable launcher scriptsThere is no separate linter/formatter; ./gradlew build is the full gate. Tests are
JUnit 5. Note Gradle's configuration cache is on (org.gradle.configuration-cache=true),
so build-script changes may need --no-configuration-cache while iterating.
Server reads the passphrase with no echo (Console.readPassword), or MINIKMS_PASSPHRASE
when there's no TTY. Both tokens come from env vars or files, never CLI args.
export MINIKMS_API_TOKEN="$(openssl rand -hex 32)" # data plane
export MINIKMS_ADMIN_TOKEN="$(openssl rand -hex 32)" # control plane
server/build/install/server/bin/server --tcp-port 9123 --keystore ~/.mini-kms/keystore.json
client/build/install/client/bin/client --tcp 127.0.0.1:9123 health # data-plane CLI
client/build/install/client/bin/kms-admin --tcp 127.0.0.1:9123 list-keys # control-plane CLI (admin token)Three Gradle modules under base package com.codeheadsystems.minikms:
core— all crypto + key management, with no socket/transport/CLI code. This separation is load-bearing: keepcorefree of I/O so the request handler and key store can be reused and swapped. Owns the wire-protocol DTOs (shared by server + client), the envelope formats, the keyring, and the request handler.server— the socket daemon (ServerMain); depends oncore.client—KmsClientlibrary plus two CLIs (ClientMain= data plane,KeyringAdminMain= control plane); depends oncore.
Every RequestType is tagged DATA or CONTROL (RequestPlane). KmsRequestHandler
(in core) routes by plane and validates the matching token — the API token for data
ops, a separate admin token for control ops — so a data client cannot manage keys.
Data-plane ops additionally pass through KeyAuthorizationPolicy per key group; the shipped
AllowAllPolicy permits any authenticated principal (the documented seam for per-client
authz later).
KmsRequestHandler depends only on two interfaces, both implemented by LocalKeyring:
MasterKeyProvider(data plane):wrap/unwrap/encrypt/decrypt/keyIdOfKeyringManager(control plane):create/rotate/list/disable/enable/destroy
Because each ciphertext carries its own kek_id inside the opaque blob, these signatures
are stable: a future remote/HSM-backed provider can drop in without touching request
handling. Preserve this boundary — don't make the handler reach past these interfaces.
passphrase --Argon2id+salt--> root key --wraps--> KEK versions --wrap--> DEKs --AES-GCM--> data
The root key and KEKs exist only as byte[] in memory and are zeroed on shutdown
(LocalKeyring.close(), wired via a shutdown hook in ServerMain). Rotation never strands
data because every blob records the exact (group, version) that wrapped it. DestroyVersion
is intentionally irreversible (crypto-shredding).
crypto/AesGcmis the only place raw symmetric crypto happens. Nonces are always fresh-random and never caller-supplied (GCM nonce reuse is catastrophic). AAD is optional ("encryption context") and bound but not stored.- Three nested binary formats:
EnvelopeFormat(version+alg+nonce+ciphertext+tag — the inner primitive),KekEnvelope(prependskek_id→ the client-facing blob), and the client-onlyFileEnvelope(MKE1container: wrapped DEK + file ciphertext). - The keystore (
keystore.json,0600) holds the salt, Argon2 params, a verification token, the root-wrapped KEKs, and an HMAC over all metadata (KeystoreIntegrity). It never contains a plaintext key. The MAC is now required on load: tampering with plaintext fields (e.g. flipping a version's status) is rejected, and a keystore written by older code without amacBase64field will fail to load.
Server binds loopback TCP + a Unix domain socket (0600 in a 0700 dir); each connection
runs on a virtual thread (KmsServer). Protocol is newline-delimited JSON, one object
per line, base64 for binary, bounded per frame (BoundedLineReader). Connections are bounded
two ways: a Semaphore connection cap and an idle-timeout watchdog (a scheduled close that
unblocks a stalled read) — both configurable. ConnectionHandler is transport-agnostic
(sees only streams), so TCP and Unix are handled identically.
corestays I/O-free. No sockets, files (beyond the keystore viaKeystore), or CLI parsing belong there.-parameterscompilation is required — Jackson deserializes the protocol/keystore records by constructor parameter names. Don't remove the compiler flag (set in the root build) or Jackson record binding breaks.- No oracles, no leaks. Any AEAD/keyring failure (wrong key/AAD, disabled/destroyed/unknown
version, malformed blob) is flattened to a single
DecryptionFailedresponse. Tokens, passphrases, keys, and request/response bodies are never logged. - Secrets via env/file, never argv. Follow the existing
resolveToken/readPassphrasepatterns; passphrases are handled aschar[]and zeroed. - Use
System.Logger(already the pattern), not a third-party logging dependency.
README.md— full architecture, flow diagrams, formats, and a glossary of terms (KEK/DEK/AEAD/etc.).docs/security/— security-review findings with before/after fixes (connection exhaustion; keystore integrity) and one open item (loopback-TCP local exposure).