From 3f5d9772516f75b411b4fc20c4fe3859f7e28605 Mon Sep 17 00:00:00 2001 From: Peter Amiri Date: Mon, 6 Jul 2026 12:34:39 -0700 Subject: [PATCH] fix(auth): route PBKDF2 getEncoded() through the SecretKey interface for Adobe 2025 JPMS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Signed-off-by: Peter Amiri --- CLAUDE.md | 1 + vendor/wheels/auth/PasswordHasher.cfc | 14 +++++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index e4c991455d..9f9a970a7f 100755 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -50,6 +50,7 @@ The framework must run on Lucee 5/6/7, Adobe CF 2018/2021/2023/2025, and BoxLang 11. **`local.X = ...` inside `catch` doesn't persist on BoxLang.** Catch body runs under a nested `local` that gets discarded on exit, so `expect(local.X)` after the catch reads the un-touched outer value. Use a struct field: `var state = {flag = false}; ... state.flag = true;`. Bare `var bareName` + unscoped `bareName = true` also works but the struct form mirrors `TenantResolverSpec` and is the prior-art pattern. 12. **`for (local.i = ...)` inside `finally` miscompiles on Lucee 7.** Lucee 7.0.1+100 throws `variable [local] doesn't exist` at runtime when a `for` loop declares or iterates `local`-/`var`-scoped variables inside a `finally` block (one probe shape even produced a JVM `Expecting a stackmap frame` verifier error). Bare assignments and function calls in `finally` are fine; loops are not. Hoist the loop into a `public` `$`-prefixed helper and call it from `finally` — reference: `$restoreEmailViewVariables()` in `vendor/wheels/controller/miscellaneous.cfc` ([#2922](https://github.com/wheels-dev/wheels/pull/2922)). 13. **Bare tag-in-script statements without parentheses (e.g. `cfabort;`) are Lucee-only.** Adobe CF compiles the bare token as a reference to an undefined VARIABLE and throws `Variable CFABORT is undefined` at runtime (every Adobe engine, not just one release). Use the script keyword (`abort;`) or the parenthesized call form (`cfheader(...)`-style) instead. The `enablePublicComponent=false` 404 branch in `vendor/wheels/Dispatch.cfc` shipped a bare `cfabort;`, which turned `GET /` on every stock Adobe install in `testing`/`production` into an HTTP 500 ([#3029](https://github.com/wheels-dev/wheels/issues/3029)). Structural guard: `vendor/wheels/tests/specs/security/BareCfabortGuardSpec.cfc` fails the suite if any bare script-context `cfabort` statement reappears under `vendor/wheels/**/*.cfc` (tag-context `` in `.cfm`/tag-based CFCs stays legal). +14. **Adobe 2025's JVM rejects member calls on JDK-internal classes (JPMS).** Calling any member on an object whose runtime class lives in an unexported package (`com.sun.*`, `jdk.internal.*`) — e.g. the `com.sun.crypto.provider.PBKDF2KeyImpl` returned by `SecretKeyFactory.generateSecret()` — throws `java.lang.reflect.InaccessibleObjectException` on Adobe 2025 (its reflection layer bulk-`setAccessible`s the concrete class's methods; Lucee, BoxLang, and Adobe ≤2023 tolerate the same call, so **local Adobe 2023 green does NOT cover this**). Route the call through the exported interface's `Method` object instead: `CreateObject("java","java.lang.Class").forName("javax.crypto.SecretKey").getMethod("getEncoded", JavaCast("null","")).invoke(keyObj, JavaCast("null",""))` — `getMethod`/`invoke` treat the null varargs as empty. Hit by `PasswordHasher.$deriveKey()` ([#3300](https://github.com/wheels-dev/wheels/issues/3300)); watch for it with any Java factory API that returns internal implementation types. Verify Adobe CF fixes locally before pushing — don't iterate via CI: ```bash diff --git a/vendor/wheels/auth/PasswordHasher.cfc b/vendor/wheels/auth/PasswordHasher.cfc index c4e2ceb9ac..efa015094e 100644 --- a/vendor/wheels/auth/PasswordHasher.cfc +++ b/vendor/wheels/auth/PasswordHasher.cfc @@ -241,7 +241,19 @@ component output="false" { try { local.factory = CreateObject("java", "javax.crypto.SecretKeyFactory").getInstance("PBKDF2WithHmacSHA256"); - local.derivedKey = local.factory.generateSecret(local.keySpec).getEncoded(); + local.secretKey = local.factory.generateSecret(local.keySpec); + + // Do NOT call members on the returned key directly: it is a + // com.sun.crypto.provider.PBKDF2KeyImpl, a JDK-internal class that + // java.base does not open. Adobe 2025's JVM rejects the reflective + // member access with InaccessibleObjectException (its reflection + // layer makes the concrete class's methods accessible en masse). + // Invoke getEncoded() through the exported javax.crypto.SecretKey + // interface instead — public interface methods need no opens. + local.getEncoded = CreateObject("java", "java.lang.Class") + .forName("javax.crypto.SecretKey") + .getMethod("getEncoded", JavaCast("null", "")); + local.derivedKey = local.getEncoded.invoke(local.secretKey, JavaCast("null", "")); } finally { // Zero the internal password copy held by the spec. local.keySpec.clearPassword();