Skip to content

Commit 47fdb0a

Browse files
bpamiriPeter Amiriclaude
authored
fix(auth): route PBKDF2 getEncoded() through the SecretKey interface for Adobe 2025 JPMS (#3301)
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 Signed-off-by: Peter Amiri <petera@pai.com> Co-authored-by: Peter Amiri <petera@pai.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent b082f4a commit 47fdb0a

2 files changed

Lines changed: 14 additions & 1 deletion

File tree

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ The framework must run on Lucee 5/6/7, Adobe CF 2018/2021/2023/2025, and BoxLang
5050
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.
5151
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)).
5252
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 `<cfabort>` in `.cfm`/tag-based CFCs stays legal).
53+
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.
5354

5455
Verify Adobe CF fixes locally before pushing — don't iterate via CI:
5556
```bash

vendor/wheels/auth/PasswordHasher.cfc

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,19 @@ component output="false" {
241241

242242
try {
243243
local.factory = CreateObject("java", "javax.crypto.SecretKeyFactory").getInstance("PBKDF2WithHmacSHA256");
244-
local.derivedKey = local.factory.generateSecret(local.keySpec).getEncoded();
244+
local.secretKey = local.factory.generateSecret(local.keySpec);
245+
246+
// Do NOT call members on the returned key directly: it is a
247+
// com.sun.crypto.provider.PBKDF2KeyImpl, a JDK-internal class that
248+
// java.base does not open. Adobe 2025's JVM rejects the reflective
249+
// member access with InaccessibleObjectException (its reflection
250+
// layer makes the concrete class's methods accessible en masse).
251+
// Invoke getEncoded() through the exported javax.crypto.SecretKey
252+
// interface instead — public interface methods need no opens.
253+
local.getEncoded = CreateObject("java", "java.lang.Class")
254+
.forName("javax.crypto.SecretKey")
255+
.getMethod("getEncoded", JavaCast("null", ""));
256+
local.derivedKey = local.getEncoded.invoke(local.secretKey, JavaCast("null", ""));
245257
} finally {
246258
// Zero the internal password copy held by the spec.
247259
local.keySpec.clearPassword();

0 commit comments

Comments
 (0)