fix(auth): route PBKDF2 getEncoded() through the SecretKey interface for Adobe 2025 JPMS - #3301
Conversation
…for Adobe 2025 JPMS SecretKeyFactory.generateSecret() returns a com.sun.crypto.provider.PBKDF2KeyImpl, a JDK-internal class java.base does not open. Adobe 2025's reflection layer bulk-setAccessibles the concrete class's methods when dispatching a member call and its JVM rejects that with InaccessibleObjectException, erroring 17 of 24 PasswordHasherSpec specs on every database leg (matrix run 28815315339) — the only new cross-engine failure from the 2962 feature merges. Lucee, BoxLang, and Adobe <= 2023 tolerate the direct call. Invoke getEncoded() via the exported javax.crypto.SecretKey interface Method object instead; public interface methods need no opens. Adds Cross-Engine Invariant 14 to CLAUDE.md so the pattern is checked going forward. Closes #3300 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Peter Amiri <petera@pai.com>
There was a problem hiding this comment.
Wheels Bot — Reviewer
TL;DR — This PR fixes #3300: PasswordHasher.$deriveKey() crashed on Adobe 2025 because it called .getEncoded() directly on the com.sun.crypto.provider.PBKDF2KeyImpl returned by SecretKeyFactory.generateSecret() — a JDK-internal class java.base does not open, which Adobe 2025's reflection layer trips over with InaccessibleObjectException. The fix routes the call through the exported javax.crypto.SecretKey interface's Method object, and documents the crash class as Cross-Engine Invariant 14. The change is minimal, correct, and empirically verified on the exact failing engine. Verdict: approve.
Correctness
The reflective path is sound. Method.invoke on the interface's getEncoded method dispatches virtually to the concrete PBKDF2KeyImpl implementation, so the returned bytes are identical to the old direct call — stored hashes are unaffected, as the PR body claims. getMethod("getEncoded", JavaCast("null","")) and invoke(key, JavaCast("null","")) correctly resolve the null varargs to "no parameters" (vendor/wheels/auth/PasswordHasher.cfc:253-256).
Cross-engine
No concern. The null-varargs interop is the one place this could have gone engine-specific, but the PR verifies 24/24 on Adobe 2025 (the failing engine), Adobe 2023, and Lucee 7, with the matrix run covering Lucee 6/7 + BoxLang. Had CFML mapped the null to a single-element [null] array, getMethod would throw NoSuchMethodException on every engine rather than pass — so the empirical green across engines confirms the intended resolution. This is exactly the kind of fix Invariant 14 is meant to prevent recurring.
Tests
Covered by the existing vendor/wheels/tests/specs/auth/PasswordHasherSpec.cfc — these are the 17/24 specs that were erroring on Adobe 2025 and now pass. A behavior-preserving cross-engine fix to an already-covered code path needs no new spec.
Docs
CLAUDE.md Invariant 14 is a clear, actionable writeup of the JPMS crash class and the "Adobe-2023-green != Adobe-2025-covered" trap (CLAUDE.md:53). The "no changelog fragment" rationale checks out: changelog.d/3155-password-hasher.added.md is still unreleased (present in changelog.d/, not yet promoted), so this correctly folds into that existing added entry rather than adding a redundant fragment.
Commits
fix(auth): route PBKDF2 getEncoded() through the SecretKey interface for Adobe 2025 JPMS — valid type/scope, 88-char header (<=100), not ALL-CAPS. Carries a Signed-off-by: trailer (DCO satisfied) and describes the why, not just the what.
Non-blocking nit (optional): Class.forName("javax.crypto.SecretKey").getMethod(...) runs on every $deriveKey() call. The reflection lookup is negligible next to PBKDF2's iteration cost, so this is purely cosmetic — but if you ever want to shave it, the Method object is immutable and could be resolved once into variables scope in init()/config(). Not required for merge.
Closes #3300 — the only new cross-engine failure from the #2962 feature merges (verified by diffing matrix run 28815315339 against the 2026-06-28 baseline: Lucee 6/7, Adobe 2023, and BoxLang show no new failures).
Root cause
SecretKeyFactory.generateSecret()returns acom.sun.crypto.provider.PBKDF2KeyImpl— a JDK-internal classjava.basedoes not open. Adobe 2025's reflection layer bulk-setAccessibles the concrete class's methods when dispatching.getEncoded()and its JVM rejects that withInaccessibleObjectException, erroring 17 of 24PasswordHasherSpecspecs on every database leg. Lucee, BoxLang, and Adobe ≤2023 tolerate the direct call — which is why local Adobe 2023 verification (24/24) missed it.Fix
Invoke
getEncoded()through the exportedjavax.crypto.SecretKeyinterfaceMethodobject — public interface methods need noopens. Output is byte-identical (same key object, same method), so stored hashes are unaffected.Also adds Cross-Engine Invariant 14 to CLAUDE.md documenting the JPMS crash class (member calls on JDK-internal implementation types) and the Adobe-2023-green-doesn't-cover-2025 trap.
Verification (local docker,
db=sqlite,testBundles=wheels.tests.specs.auth.PasswordHasherSpec)tools/docker/adobe2025)No changelog fragment: #3288 has not shipped in a tagged release, so this folds into its existing
addedentry.🤖 Generated with Claude Code