Skip to content

Commit 6a70dbb

Browse files
committed
adding CLAUDE.md
1 parent dc19a6f commit 6a70dbb

2 files changed

Lines changed: 94 additions & 0 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,5 @@ idea/
3636

3737
# local
3838
.claude/
39+
CLAUDE.local.md
3940
dev/

CLAUDE.md

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
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

Comments
 (0)