|
| 1 | +# CLAUDE.md |
| 2 | + |
| 3 | +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. |
| 4 | + |
| 5 | +## Build Commands |
| 6 | + |
| 7 | +Gradle requires JDK 17+. Set `JAVA_HOME` to your JDK 17 installation before running any Gradle task. |
| 8 | + |
| 9 | +| Task | Command | |
| 10 | +|------|---------| |
| 11 | +| Build + test | `./gradlew build` | |
| 12 | +| Build + produce distributable jar | `./gradlew dist` | |
| 13 | +| Run tests only | `./gradlew test` | |
| 14 | +| Run a single test | `./gradlew test --tests "jadx.plugins.stringdecoder.JadxStringDecoderPluginTest.integrationTest"` | |
| 15 | + |
| 16 | +The distributable jar lands in `build/dist/`. |
| 17 | + |
| 18 | +## Architecture |
| 19 | + |
| 20 | +A JADX decompiler plugin that detects likely Base64-encoded string constants and byte array fields, adding inline comments with their decoded values. The plugin entry point is declared via Java SPI in `src/main/resources/META-INF/services/jadx.api.plugins.JadxPlugin`. |
| 21 | + |
| 22 | +### Plugin lifecycle |
| 23 | + |
| 24 | +``` |
| 25 | +JadxStringDecoderPlugin.init() |
| 26 | + → registers B64DeobfuscateOptions |
| 27 | + → registers B64DeobfuscatePass (method-body Base64 string detection) |
| 28 | + → registers B64FieldInitPass (field initializer Base64 detection) |
| 29 | + → registers ByteArrayStringPass (byte[] field string detection) |
| 30 | +``` |
| 31 | + |
| 32 | +All three passes are skipped when `enable` is false. |
| 33 | + |
| 34 | +### Pass overview |
| 35 | + |
| 36 | +**`B64DeobfuscatePass`** — runs `.after("SSATransform").before("ConstInlineVisitor")` |
| 37 | + |
| 38 | +Iterates every method's basic blocks looking for `ConstStringNode` instructions. On a likely-Base64 string it attaches a `CodeComment` to the instruction. `ConstInlineVisitor` later inlines the const into its consuming instruction and calls `inheritMetadata`, which propagates the comment. Code generation in `InsnGen`/`RegionGen` calls `CodeGenUtils.addCodeComments` to render it on the same line. |
| 39 | + |
| 40 | +Also detects when a string is a direct argument to a `Base64.decode`-like call (via `SSAVar.getUseList()`) and bypasses false-positive heuristics in that case — the call itself is strong evidence of intent. |
| 41 | + |
| 42 | +**`B64FieldInitPass`** — runs `.after("ExtractFieldInit")` |
| 43 | + |
| 44 | +Handles two field patterns: |
| 45 | +- *`FIELD_INIT_INSN`* — field set via `<clinit>` or constructor; `ExtractFieldInit` extracts the init expression. If the init is a direct `ConstStringNode`, detects normally. If it's a complex expression (e.g. `new String(Base64.decode("...", 0))`), walks the instruction arg tree recursively via `RegisterArg.getAssignInsn()` / `InsnWrapArg.getWrapInsn()` to find the embedded string. When the string is a direct arg to a `Base64.decode`-like `InvokeNode`, forces decode without heuristic checks. |
| 46 | +- *`CONSTANT_VALUE`* — `static final` field with a literal string value encoded in the class file; retrieved via `JadxAttrType.CONSTANT_VALUE` / `EncodedValue`. |
| 47 | + |
| 48 | +Comments are added via `field.addCodeComment()` which renders on a separate line above the field declaration (`FieldNode implements ICodeNode` → `startNewLine = true` in `CodeGenUtils`). |
| 49 | + |
| 50 | +**`ByteArrayStringPass`** — runs `.after("ExtractFieldInit")` |
| 51 | + |
| 52 | +Looks for `byte[]` fields initialised with a `FilledNewArrayNode` of all literal bytes. If the bytes decode as valid UTF-8 with sufficient printable characters, adds a `bytes: "..."` comment on the field. |
| 53 | + |
| 54 | +### Detection logic (`B64Detector`) |
| 55 | + |
| 56 | +`B64Detector.detect(String, B64DeobfuscateOptions)` applies a pipeline of filters (all thresholds configurable via options): |
| 57 | + |
| 58 | +1. Minimum encoded length (`minInputLength`, default 8) |
| 59 | +2. Require `=` padding suffix (`requirePadding`, default false) |
| 60 | +3. Skip identifier-like strings matching `^[A-Za-z][A-Za-z0-9]*$` (`skipIdentifiers`, default false) |
| 61 | +4. Charset validation — must match standard or URL-safe Base64 alphabet |
| 62 | +5. Attempt decode with `Base64.getDecoder()`, then `Base64.getUrlDecoder()` |
| 63 | +6. Strict UTF-8 decode (`CodingErrorAction.REPORT`) |
| 64 | +7. Minimum printable-ASCII ratio (`minPrintablePercent`, default 90%) |
| 65 | +8. Minimum alphanumeric ratio (`minAlphanumericPercent`, default 35%) |
| 66 | +9. Minimum decoded length (`minDecodedLength`, default 0 = disabled) |
| 67 | +10. Truncation (`maxCommentLength`, default 100; 0 = unlimited) |
| 68 | + |
| 69 | +`B64Detector.decodeForced(String, int)` skips steps 1–9 and uses `CodingErrorAction.REPLACE` instead of `REPORT`. Used when the string is an explicit arg to a `Base64.decode` call. |
| 70 | + |
| 71 | +### False-positive prevention |
| 72 | + |
| 73 | +The `<clinit>` / field-init skip: `B64DeobfuscatePass` collects all `CONSTANT_VALUE` field string literals in the class (lazily, on the first B64 hit) and skips any `ConstStringNode` whose value matches — those are handled by `B64FieldInitPass` to avoid double annotation. |
| 74 | + |
| 75 | +### Key JADX API types |
| 76 | + |
| 77 | +| Type | Notes | |
| 78 | +|------|-------| |
| 79 | +| `JadxDecompilePass` | Interface for decompile-phase passes; `visit(ClassNode)` returns `true` to enable method traversal | |
| 80 | +| `ConstStringNode` | `InsnNode` subtype for `CONST_STR`; use `.getString()` | |
| 81 | +| `InvokeNode` | `InsnNode` subtype for method calls; use `.getCallMth()` → `MethodInfo` | |
| 82 | +| `FilledNewArrayNode` | `InsnNode` subtype for `filled-new-array`; use `.getElemType()` and `.getArg(i)` | |
| 83 | +| `AType.CODE_COMMENTS` | `AttrList<CodeComment>` — add with `insn.addAttr(AType.CODE_COMMENTS, new CodeComment(text, CommentStyle.LINE))`. `InsnNode` does **not** extend `NotificationAttrNode`, so use `addAttr` directly | |
| 84 | +| `FieldNode.addCodeComment(text)` | Convenience method that renders a comment on a line above the field declaration | |
| 85 | +| `OrderedJadxPassInfo` | Supports `.before(name)` and `.after(name)`; names are JADX internal visitor class names, not `@JadxVisitor` name attributes | |
| 86 | +| `AType.FIELD_INIT_INSN` / `FieldInitInsnAttr` | Set by `ExtractFieldInit` on fields whose init is extracted from `<clinit>` or a constructor | |
| 87 | +| `JadxAttrType.CONSTANT_VALUE` / `EncodedValue` | Literal field value encoded in the class file; check `EncodedType.ENCODED_STRING` before casting | |
| 88 | +| `RegisterArg.getAssignInsn()` | Follows a register back to the instruction that assigned it (SSA form) | |
| 89 | +| `SSAVar.getUseList()` | All uses of an SSA register — used to find the instruction consuming a `ConstStringNode`'s result | |
| 90 | + |
| 91 | +### jadx-core dependency |
| 92 | + |
| 93 | +`jadx-core` is `compileOnly` — excluded from the output jar. The plugin jar is loaded into an existing JADX installation at runtime. |
0 commit comments