Class AeadRequestStateCodec
- All Implemented Interfaces:
RequestStateCodec
RequestStateCodec: AES-256-GCM keyed by a pluggable KeyProvider.
The no-arg constructor uses a fresh, per-process EphemeralKeyProvider — not restart-durable
and not shareable across process instances by design. Each instance generates its own random key and
keyId at construction time; a token sealed by one instance can never be unsealed by another
(including the same process after a restart). Operators who need cross-restart or multi-instance resumption
must supply a shared/rotating KeyProvider (e.g. StaticKeyProvider) via
AeadRequestStateCodec(KeyProvider) instead of relying on this default. This is a documented, tested
property (see AeadRequestStateCodec_Test), not an accidental limitation.
Sealed-token format (all opaque to callers, versioned so it can evolve):
version '.' base64url(keyId) '.' base64url(nonce) '.' base64url(ciphertext+tag). The current
version literal is "1"; an unrecognized version fails unseal(java.lang.String, java.lang.String, java.security.Principal) closed. The
keyId segment is cleartext base64url of the UTF-8 keyId bytes — it has to be read
before the key it names can be resolved for decryption — but it is folded into the AEAD's authenticated
data, so a swapped keyId fails the GCM tag check exactly like any other tamper. The plaintext is the
JSON serialization of the McpRequestState record.
The AAD passed to seal(org.apache.juneau.rest.server.mcp.v20260728.McpRequestState, java.lang.String, java.security.Principal)/unseal(java.lang.String, java.lang.String, java.security.Principal) is authenticated but never encrypted (standard AES-GCM AAD
semantics). The dispatcher passes the canonical method + ' ' + protocolVersion() + ' ' +
target (NUL-separated) form as the caller-supplied AAD (see McpRevision#aad) — the trailing
target field (the tool/prompt name or resource uri) binds the token to the specific
operation it paused against, not merely the method, so it can't be resumed against a different
tool/prompt/resource. This codec composes that caller-supplied aad, the KeyProvider's
keyId (per its implicit keyId-authentication contract), and principalIdentity(principal)
(the principal binding described below) into the cipher's actual AAD using the same self-delimiting,
length-prefixed framing described on principalIdentity(Principal) — so the three-field outer
composition is unambiguous exactly as the two-field inner one is, even if a field happened to contain a NUL.
Principal-bound AAD. seal(org.apache.juneau.rest.server.mcp.v20260728.McpRequestState, java.lang.String, java.security.Principal)/unseal(java.lang.String, java.lang.String, java.security.Principal) fold the caller's authenticated
Principal identity into the AEAD's authenticated data, so a requestState minted for one caller
cannot be resumed by another: a mismatched principal fails the GCM tag check and unseal(java.lang.String, java.lang.String, java.security.Principal) returns
Optional.empty(). The bound identity is a canonical, deterministic iss|sub (issuer + subject)
string derived by principalIdentity(Principal) — issuer-scoped so a bare sub cannot
collide across IdPs. A "anonymous" sentinel rather than skipping the binding, so an anonymous-sealed token and an
authenticated-sealed token are never mutually resumable (anonymous↔anonymous still round-trips). See
principalIdentity(Principal) for the exact canonical form and its collision-safety guarantee.
Random per-seal nonces are safe up to roughly 2^32 seals under a single key (the AES-GCM birthday bound). The
ephemeral default's key is per-process and never approaches that. A long-lived custom KeyProvider key
(e.g. a StaticKeyProvider entry left in place for months) is the case where this bound actually
matters — rotate to a fresh keyId well before it, not after.
unseal(java.lang.String, java.lang.String, java.security.Principal) is wired to untrusted, client-supplied requestState strings, so it applies defensive
length guards (a plausibly-sized token, and a bounded keyId segment) before any base64 decode; an
out-of-bounds input returns Optional.empty() rather than allocating from an attacker-controlled
length.
-
Constructor Summary
Constructors -
Method Summary
Modifier and TypeMethodDescriptionseal(McpRequestState state, String aad, Principal principal) Seals a payload into an opaque token, exposing the authenticated caller identity to the codec.Unseals a token, verifying integrity against the supplied AAD, with the authenticated caller identity available to the codec.Methods inherited from class java.lang.Object
clone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, waitMethods inherited from interface org.apache.juneau.rest.server.mcp.v20260728.RequestStateCodec
seal, unseal
-
Constructor Details
-
AeadRequestStateCodec
public AeadRequestStateCodec()Constructor. Uses a fresh, per-processEphemeralKeyProvider— the zero-config, non-shareable, non-durable default described in the class Javadoc. -
AeadRequestStateCodec
Constructor.- Parameters:
keyProvider- The source of sealing/resolving keys. Must not benull .
-
-
Method Details
-
seal
Description copied from interface:RequestStateCodecSeals a payload into an opaque token, exposing the authenticated caller identity to the codec.- Specified by:
sealin interfaceRequestStateCodec- Parameters:
state- The payload to seal. Must not benull .aad- Additional authenticated data (the dispatcher passes the canonicalmethod + ' ' + protocolVersion + ' ' + targetform — see the class Javadoc). Must not benull .principal- The authenticated caller (see the class Javadoc). May benull for an anonymous caller or when resource-server auth is disabled.- Returns:
- The opaque token. Never
null .
-
unseal
Description copied from interface:RequestStateCodecUnseals a token, verifying integrity against the supplied AAD, with the authenticated caller identity available to the codec.The
tokenis arbitrary, untrusted client input. It arrives verbatim from the follow-up request'srequestStatefield, so an implementation must assume nothing about its length or content: it must not allocate buffers sized from the raw token, and it must not throw — any malformed, oversized, tampered, or AAD-mismatched input must returnOptional.empty()(see the class Javadoc contract).- Specified by:
unsealin interfaceRequestStateCodec- Parameters:
token- The token previously returned byRequestStateCodec.seal(McpRequestState, String, Principal). Must not benull .aad- The AAD to verify against. Must match the value passed toRequestStateCodec.seal(McpRequestState, String, Principal)exactly, or verification fails.principal- The authenticated caller (see the class Javadoc). May benull for an anonymous caller or when resource-server auth is disabled. A codec that binds the principal must fail verification when it differs from the sealing principal.- Returns:
- The original payload, or
Optional.empty()if verification fails for any reason (tamper, AAD mismatch, malformed token, principal mismatch).
-