33import org .objectweb .asm .ClassReader ;
44import org .objectweb .asm .ClassVisitor ;
55import org .objectweb .asm .ClassWriter ;
6+ import org .objectweb .asm .Label ;
67import org .objectweb .asm .MethodVisitor ;
78import org .objectweb .asm .Opcodes ;
89import org .objectweb .asm .Type ;
910
1011import java .nio .charset .StandardCharsets ;
12+ import java .util .ArrayList ;
13+ import java .util .List ;
1114import java .util .Random ;
1215
1316/**
14- * Obfuscates string literals in bytecode using XOR encryption.
15- * Replaces LDC "string" with inline decryption bytecode at each call site.
16- * No central decoder class – no single point to hook and dump all strings.
17- * Key per class (owner.hashCode) and per string (index in method) for stronger obfuscation.
17+ * Obfuscates string literals using XOR encryption.
18+ * Replaces LDC "string" with inline decryption at each use site (no central decoder).
19+ * Also handles static final String fields: removes ConstantValue and initializes via
20+ * obfuscated code in <clinit>, so API keys and secrets are never readable.
21+ * Key per class and per string/field for stronger obfuscation.
1822 */
1923public final class StringObfuscator {
2024
21- /** Local variable indices for inline decrypt (high to avoid clashes with method params). */
2225 private static final int LOCAL_ENC = 100 ;
2326 private static final int LOCAL_KEY = 101 ;
2427 private static final int LOCAL_OUT = 102 ;
@@ -29,16 +32,10 @@ public final class StringObfuscator {
2932
3033 private final int key ;
3134
32- /**
33- * Creates an obfuscator with default (fixed) key.
34- */
3535 public StringObfuscator () {
3636 this .key = DEFAULT_KEY ;
3737 }
3838
39- /**
40- * Creates an obfuscator with random key (different per instance).
41- */
4239 public static StringObfuscator withRandomKey () {
4340 return new StringObfuscator (new Random ().nextInt ());
4441 }
@@ -47,11 +44,11 @@ private StringObfuscator(int key) {
4744 this .key = key ;
4845 }
4946
50- /**
51- * Encrypts a string to a byte array using XOR.
52- */
53- public byte [] encrypt ( String s ) {
54- return encrypt ( s , key );
47+ public byte [] transform ( byte [] classBytes ) {
48+ ClassReader reader = new ClassReader ( classBytes );
49+ ClassWriter writer = new ClassWriter ( reader , ClassWriter . COMPUTE_FRAMES );
50+ reader . accept ( new StringObfuscatorClassVisitor ( writer , key ), ClassReader . EXPAND_FRAMES );
51+ return writer . toByteArray ( );
5552 }
5653
5754 private static byte [] encrypt (String s , int key ) {
@@ -63,20 +60,12 @@ private static byte[] encrypt(String s, int key) {
6360 return out ;
6461 }
6562
66- /**
67- * Transforms the class bytes, obfuscating string literals.
68- */
69- public byte [] transform (byte [] classBytes ) {
70- ClassReader reader = new ClassReader (classBytes );
71- ClassWriter writer = new ClassWriter (reader , ClassWriter .COMPUTE_FRAMES );
72- reader .accept (new StringObfuscatorClassVisitor (writer , key ), ClassReader .EXPAND_FRAMES );
73- return writer .toByteArray ();
74- }
75-
7663 private static final class StringObfuscatorClassVisitor extends ClassVisitor {
7764
7865 private final int key ;
7966 private String owner ;
67+ private final List <StaticStringField > staticStringFields = new ArrayList <>();
68+ private boolean hasClinit ;
8069
8170 StringObfuscatorClassVisitor (ClassVisitor cv , int key ) {
8271 super (Opcodes .ASM9 , cv );
@@ -89,12 +78,80 @@ public void visit(int version, int access, String name, String signature, String
8978 super .visit (version , access , name , signature , superName , interfaces );
9079 }
9180
81+ @ Override
82+ public org .objectweb .asm .FieldVisitor visitField (int access , String name , String descriptor ,
83+ String signature , Object value ) {
84+ if ("Ljava/lang/String;" .equals (descriptor )
85+ && (access & Opcodes .ACC_STATIC ) != 0
86+ && (access & Opcodes .ACC_FINAL ) != 0
87+ && value instanceof String s ) {
88+ staticStringFields .add (new StaticStringField (name , s ));
89+ return super .visitField (access , name , descriptor , signature , null );
90+ }
91+ return super .visitField (access , name , descriptor , signature , value );
92+ }
93+
9294 @ Override
9395 public MethodVisitor visitMethod (int access , String name , String descriptor ,
9496 String signature , String [] exceptions ) {
9597 MethodVisitor mv = super .visitMethod (access , name , descriptor , signature , exceptions );
98+ if ("<clinit>" .equals (name ) && !staticStringFields .isEmpty ()) {
99+ hasClinit = true ;
100+ return new ClinitPrepender (mv , key , owner , staticStringFields );
101+ }
96102 return new StringObfuscatorMethodVisitor (mv , key , owner );
97103 }
104+
105+ @ Override
106+ public void visitEnd () {
107+ if (!staticStringFields .isEmpty () && !hasClinit ) {
108+ MethodVisitor mv = super .visitMethod (Opcodes .ACC_STATIC , "<clinit>" , "()V" , null , null );
109+ if (mv != null ) {
110+ mv .visitCode ();
111+ emitStaticFieldInits (mv , key , owner , staticStringFields );
112+ mv .visitInsn (Opcodes .RETURN );
113+ mv .visitMaxs (0 , 0 );
114+ mv .visitEnd ();
115+ }
116+ }
117+ super .visitEnd ();
118+ }
119+
120+ private record StaticStringField (String name , String value ) {}
121+ }
122+
123+ private static final class ClinitPrepender extends MethodVisitor {
124+
125+ private final int key ;
126+ private final String owner ;
127+ private final List <StringObfuscatorClassVisitor .StaticStringField > fields ;
128+
129+ ClinitPrepender (MethodVisitor mv , int key , String owner ,
130+ List <StringObfuscatorClassVisitor .StaticStringField > fields ) {
131+ super (Opcodes .ASM9 , mv );
132+ this .key = key ;
133+ this .owner = owner ;
134+ this .fields = fields ;
135+ }
136+
137+ @ Override
138+ public void visitCode () {
139+ emitStaticFieldInits (this , key , owner , fields );
140+ super .visitCode ();
141+ }
142+ }
143+
144+ private static void emitStaticFieldInits (MethodVisitor mv , int key , String owner ,
145+ List <StringObfuscatorClassVisitor .StaticStringField > fields ) {
146+ for (int i = 0 ; i < fields .size (); i ++) {
147+ var f = fields .get (i );
148+ int actualKey = key ^ owner .hashCode () ^ i ;
149+ byte [] enc = encrypt (f .value (), actualKey );
150+ emitRuntimeKey (mv , owner , key ^ i );
151+ emitByteArray (mv , enc );
152+ emitInlineDecrypt (mv );
153+ mv .visitFieldInsn (Opcodes .PUTSTATIC , owner , f .name (), "Ljava/lang/String;" );
154+ }
98155 }
99156
100157 private static final class StringObfuscatorMethodVisitor extends MethodVisitor {
@@ -112,25 +169,21 @@ private static final class StringObfuscatorMethodVisitor extends MethodVisitor {
112169 @ Override
113170 public void visitLdcInsn (Object value ) {
114171 if (value instanceof String s ) {
115- emitEncryptedString (s );
172+ int idx = stringIndex ++;
173+ int actualKey = key ^ owner .hashCode () ^ idx ;
174+ byte [] encrypted = encrypt (s , actualKey );
175+ emitRuntimeKey (idx );
176+ emitByteArray (encrypted );
177+ emitInlineDecrypt ();
116178 } else {
117179 super .visitLdcInsn (value );
118180 }
119181 }
120182
121- private void emitEncryptedString (String s ) {
122- int idx = stringIndex ++;
123- int actualKey = key ^ owner .hashCode () ^ idx ;
124- byte [] encrypted = encrypt (s , actualKey );
125- emitByteArray (encrypted );
126- emitRuntimeKey (idx );
127- emitInlineDecrypt ();
128- }
129-
130- /** Inline XOR decrypt: stack [byte[] encrypted, int key] -> stack [String]. No central decoder. */
131183 private void emitInlineDecrypt () {
132- org .objectweb .asm .Label loopStart = new org .objectweb .asm .Label ();
133- org .objectweb .asm .Label loopEnd = new org .objectweb .asm .Label ();
184+ Label loopStart = new Label ();
185+ Label loopEnd = new Label ();
186+ // Stack: [key, byte[]] - top is byte[], store it first (ASTORE), then key (ISTORE)
134187 super .visitVarInsn (Opcodes .ASTORE , LOCAL_ENC );
135188 super .visitVarInsn (Opcodes .ISTORE , LOCAL_KEY );
136189 super .visitVarInsn (Opcodes .ALOAD , LOCAL_ENC );
@@ -169,7 +222,6 @@ private void emitInlineDecrypt() {
169222 super .visitMethodInsn (Opcodes .INVOKESPECIAL , "java/lang/String" , "<init>" , "([BLjava/nio/charset/Charset;)V" , false );
170223 }
171224
172- /** Emits: (key ^ stringIndex) ^ thisClass.getName().hashCode() = actualKey per class and per string */
173225 private void emitRuntimeKey (int stringIndex ) {
174226 super .visitLdcInsn (Type .getObjectType (owner ));
175227 super .visitMethodInsn (Opcodes .INVOKEVIRTUAL , "java/lang/Class" , "getName" , "()Ljava/lang/String;" , false );
@@ -194,4 +246,69 @@ private void emitByteArray(byte[] bytes) {
194246 }
195247 }
196248 }
249+
250+ private static void emitByteArray (MethodVisitor mv , byte [] bytes ) {
251+ mv .visitLdcInsn (bytes .length );
252+ mv .visitIntInsn (Opcodes .NEWARRAY , Opcodes .T_BYTE );
253+ for (int i = 0 ; i < bytes .length ; i ++) {
254+ mv .visitInsn (Opcodes .DUP );
255+ mv .visitLdcInsn (i );
256+ int b = bytes [i ] & 0xFF ;
257+ if (b <= 127 ) {
258+ mv .visitIntInsn (Opcodes .BIPUSH , b );
259+ } else {
260+ mv .visitIntInsn (Opcodes .SIPUSH , b );
261+ }
262+ mv .visitInsn (Opcodes .BASTORE );
263+ }
264+ }
265+
266+ private static void emitRuntimeKey (MethodVisitor mv , String owner , int actualKey ) {
267+ mv .visitLdcInsn (Type .getObjectType (owner ));
268+ mv .visitMethodInsn (Opcodes .INVOKEVIRTUAL , "java/lang/Class" , "getName" , "()Ljava/lang/String;" , false );
269+ mv .visitMethodInsn (Opcodes .INVOKEVIRTUAL , "java/lang/String" , "hashCode" , "()I" , false );
270+ mv .visitLdcInsn (actualKey );
271+ mv .visitInsn (Opcodes .IXOR );
272+ }
273+
274+ private static void emitInlineDecrypt (MethodVisitor mv ) {
275+ Label loopStart = new Label ();
276+ Label loopEnd = new Label ();
277+ mv .visitVarInsn (Opcodes .ASTORE , LOCAL_ENC );
278+ mv .visitVarInsn (Opcodes .ISTORE , LOCAL_KEY );
279+ mv .visitVarInsn (Opcodes .ALOAD , LOCAL_ENC );
280+ mv .visitInsn (Opcodes .ARRAYLENGTH );
281+ mv .visitIntInsn (Opcodes .NEWARRAY , Opcodes .T_BYTE );
282+ mv .visitVarInsn (Opcodes .ASTORE , LOCAL_OUT );
283+ mv .visitInsn (Opcodes .ICONST_0 );
284+ mv .visitVarInsn (Opcodes .ISTORE , LOCAL_I );
285+ mv .visitLabel (loopStart );
286+ mv .visitVarInsn (Opcodes .ILOAD , LOCAL_I );
287+ mv .visitVarInsn (Opcodes .ALOAD , LOCAL_ENC );
288+ mv .visitInsn (Opcodes .ARRAYLENGTH );
289+ mv .visitJumpInsn (Opcodes .IF_ICMPGE , loopEnd );
290+ mv .visitVarInsn (Opcodes .ALOAD , LOCAL_ENC );
291+ mv .visitVarInsn (Opcodes .ILOAD , LOCAL_I );
292+ mv .visitInsn (Opcodes .BALOAD );
293+ mv .visitVarInsn (Opcodes .ILOAD , LOCAL_KEY );
294+ mv .visitVarInsn (Opcodes .ILOAD , LOCAL_I );
295+ mv .visitInsn (Opcodes .IADD );
296+ mv .visitIntInsn (Opcodes .SIPUSH , 255 );
297+ mv .visitInsn (Opcodes .IAND );
298+ mv .visitInsn (Opcodes .IXOR );
299+ mv .visitInsn (Opcodes .I2B );
300+ mv .visitVarInsn (Opcodes .ISTORE , LOCAL_BYTE );
301+ mv .visitVarInsn (Opcodes .ALOAD , LOCAL_OUT );
302+ mv .visitVarInsn (Opcodes .ILOAD , LOCAL_I );
303+ mv .visitVarInsn (Opcodes .ILOAD , LOCAL_BYTE );
304+ mv .visitInsn (Opcodes .BASTORE );
305+ mv .visitIincInsn (LOCAL_I , 1 );
306+ mv .visitJumpInsn (Opcodes .GOTO , loopStart );
307+ mv .visitLabel (loopEnd );
308+ mv .visitTypeInsn (Opcodes .NEW , "java/lang/String" );
309+ mv .visitInsn (Opcodes .DUP );
310+ mv .visitVarInsn (Opcodes .ALOAD , LOCAL_OUT );
311+ mv .visitFieldInsn (Opcodes .GETSTATIC , "java/nio/charset/StandardCharsets" , "UTF_8" , "Ljava/nio/charset/Charset;" );
312+ mv .visitMethodInsn (Opcodes .INVOKESPECIAL , "java/lang/String" , "<init>" , "([BLjava/nio/charset/Charset;)V" , false );
313+ }
197314}
0 commit comments