Obfuscating Your Plugins with ProGuard

Wuzi

September 2023, 24

7 min read

Obfuscating Your Plugins with ProGuard

The plugin obfuscation has been officially described in great detail, see: obfuscate the plugin

So without further ado, this post will be a step-by-step demonstration of obfuscating your plugin with ProGuard, starting with a real-world example.

To see the comparison file, the key code of ProGuard obfuscation is added to this commit, all the code for this post in this repository

Added ProGuard

In build.gradle.kts, add the buildscript node to import the proguard dependency

1
buildscript {
2
repositories {
3
maven {
4
setUrl("https://maven.aliyun.com/repository/public/")
5
setUrl("https://maven.aliyun.com/nexus/content/groups/public/")
6
setUrl("https://plugins.gradle.org/m2/")
7
setUrl("https://oss.sonatype.org/content/repositories/snapshots/")
8
}
9
mavenCentral()
10
gradlePluginPortal()
11
}
12
13
dependencies {
14
classpath("com.guardsquare:proguard-gradle:7.3.2")
15
}
16
}

Configuring ProGuard

Register a new task in build.gradle.kts:

1
// Register a new task, task name is "proguard"
2
register<proguard.gradle.ProGuardTask>("proguard") {
3
dependsOn(instrumentedJar)
4
verbose()
5
6
val javaHome = System.getProperty("java.home")
7
File("$javaHome/jmods/").listFiles()!!.forEach { libraryjars(it.absolutePath)}
8
9
// Use the jar task output as a input jar. This will automatically add the necessary task dependency.
10
injars("build/libs/instrumented-${properties("pluginName")}-${properties("pluginVersion")}.jar")
11
outjars("build/obfuscated/output/instrumented-${properties("pluginName")}-${properties("pluginVersion")}.jar")
12
13
14
libraryjars(configurations.compileClasspath.get())
15
16
dontshrink()
17
dontoptimize()
18
19
adaptclassstrings("**.xml")
20
adaptresourcefilecontents("**.xml")
21
22
// Allow methods with the same signature, except for the return type,
23
// to get the same obfuscation name.
24
overloadaggressively()
25
26
// Put all obfuscated classes into the nameless root package.
27
repackageclasses("")
28
dontwarn()
29
30
printmapping("build/obfuscated/output/${properties("pluginName")}-${properties("pluginVersion")}-ProGuard-ChangeLog.txt")
31
32
target(properties("pluginVersion"))
33
34
adaptresourcefilenames()
35
optimizationpasses(9)
36
allowaccessmodification()
37
38
keepattributes("Exceptions,InnerClasses,Signature,Deprecated,SourceFile,LineNumberTable,*Annotation*,EnclosingMethod")
39
40
keep("""
41
class * implements com.intellij.openapi.components.PersistentStateComponent {*;}
42
""".trimIndent()
43
)
44
45
keepclassmembers("""
46
class * {public static ** INSTANCE;}
47
""".trimIndent()
48
)
49
keep("class com.intellij.util.* {*;}")
50
}
51
52
53
prepareSandbox {
54
if (properties("enableProGuard").toBoolean()) {
55
dependsOn("proguard")
56
pluginJar.set(File("build/obfuscated/output/instrumented-${properties("pluginName")}-${properties("pluginVersion")}.jar"))
57
}
58
}

Build test

After the configuration is complete, run the plugin and the debug message is as follows:

Terminal window
1
21:40:12: Executing 'runIde'...
2
3
Starting Gradle Daemon...
4
Connected to the target VM, address: '127.0.0.1:12563', transport: 'socket'
5
Gradle Daemon started in 915 ms
6
> Task :initializeIntelliJPlugin
7
> Task :patchPluginXml UP-TO-DATE
8
9
> Task :verifyPluginConfiguration
10
[gradle-intellij-plugin :verifyPluginConfiguration] The following plugin configuration issues were found:
11
- The Java configuration specifies sourceCompatibility=11 but IntelliJ Platform 2022.3.3 requires sourceCompatibility=17.
12
See: https://jb.gg/intellij-platform-versions
13
14
> Task :compileKotlin NO-SOURCE
15
> Task :compileJava UP-TO-DATE
16
> Task :processResources UP-TO-DATE
17
> Task :classes UP-TO-DATE
18
> Task :setupInstrumentCode
19
> Task :instrumentCode UP-TO-DATE
20
> Task :jar UP-TO-DATE
21
> Task :inspectClassesForKotlinIC UP-TO-DATE
22
> Task :instrumentedJar UP-TO-DATE
23
> Task :proguard <=== proguard task 已经成功执行。
24
> Task :prepareSandbox
25
Disconnected from the target VM, address: '127.0.0.1:12563', transport: 'socket'
26
Connected to the target VM, address: 'localhost:12616', transport: 'socket'

Obfuscated results

After obfuscated:

On the left side, file names have been obfuscated, and on the right side, file class names, method names, and variable names have been obfuscated.

Special circumstances

In IntelliJ plugin projects, there are some files that are not to be obfuscated. Two cases are listed in this demo.

Called via reflection

One of the files in the project is MyDefaultFileType.java. This file adds a new file type, let’s call it a J-file, with a *.j or *.J file extension.

This file has already been registered in plugin.xml

1
<extensions defaultExtensionNs="com.intellij">
2
...
3
<fileType name="MyNativeFile" implementationClass="com.obiscr.template.MyDefaultFileType" fieldName="INSTANCE"
4
extensions="j;J" order="first"/>
5
...
6
</extensions>

The key point here is a fieldName attribute: INSTANCE, which corresponds to INSTANCE in MyDefaultFileType.java. In other words, they have to be the same. Otherwise the property will not be found when the call is reflected off.

For example, once we’ve removed the proguard task’s

1
register<proguard.gradle.ProGuardTask>("proguard") {
2
...
3
4
// keepclassmembers("""
5
// class * {public static ** INSTANCE;}
6
// """.trimIndent()
7
// )
8
...
9
}

Run the plugin and you get an error:

Terminal window
1
2023-09-24 21:31:18,647 [ 3018] WARN - #c.i.e.RunManager - Must be not called before project components initialized
2
Info | RdCoroutineScope | 53:DefaultDispatcher-worker-35 | RdCoroutineHost overridden
3
2023-09-24 21:31:21,580 [ 5951] SEVERE - #c.i.o.f.i.FileTypeManagerImpl - INSTANCE
4
java.lang.NoSuchFieldException: INSTANCE
5
at java.base/java.lang.Class.getDeclaredField(Class.java:2610)
6
at com.intellij.openapi.fileTypes.impl.FileTypeManagerImpl.instantiateFileTypeBean(FileTypeManagerImpl.java:492)
7
at com.intellij.openapi.fileTypes.impl.FileTypeManagerImpl.mergeOrInstantiateFileTypeBean(FileTypeManagerImpl.java:463)

It shows that the INSTANCE attribute could not be found. So for this case, you can use proguard’s keepclassmembers to keep the INSTANCE attribute from being obfuscated in all classes. Of course, you can also keep INSTANCE from being obfuscated in all classes that implements INativeFileType if you want. This is a more precise control.

You may ask, since this problem can be avoided by keeping the fieldName in plugin.xml the same as INSTANCE in MyDefaultFileType.java, wouldn’t it be possible to avoid this problem by changing it to the same value?

Yes, this is true in principle. However, the obfuscation generates a random variable name each time. That is, the attribute fieldName must have been specified as a certain exact value first, and then the packaging started, and in the class file after packaging, the INSTANCE is not necessarily the value specified above. Therefore, we still need to exclude INSTANCE from the obfuscation rules to avoid this problem.

Local data storage

In many cases, there is a need to store some data locally, such as settings data, environment data, etc. In this case, you need to use @State to specify a file to store the data. In this case, you need to use @State to specify the storage file. For example, the

MyState.java
1
package com.obiscr.template;
2
3
import com.intellij.openapi.components.*;
4
import com.intellij.openapi.project.Project;
5
import com.intellij.util.xmlb.XmlSerializerUtil;
6
import org.jetbrains.annotations.NotNull;
7
import org.jetbrains.annotations.Nullable;
8
9
@State( name = "com.obiscr.template.MyState", storages = @Storage("my-state/state.xml"))
10
public class MyState implements PersistentStateComponent<MyState> {
11
public String currentVersion = "";
12
public Boolean enableFeature = true;
13
public String myCustomKey = "";
14
15
public static MyState getInstance(@NotNull Project project) {
16
return project.getService(MyState.class);
17
}
18
19
@Nullable
20
@Override
21
public MyState getState() {
22
return this;
23
}
24
25
@Override
26
public void loadState(@NotNull MyState state) {
27
XmlSerializerUtil.copyBean(state, this);
28
}
29
30
}

Here a storage file and some properties are defined, it will and a new state.xml file is created in the my-state directory of the project’s .idea directory, with the following contents

1
<?xml version="1.0" encoding="UTF-8"?>
2
<project version="4">
3
<component name="com.obiscr.template.MyState">
4
<option name="currentVersion" value="v1.0.1" />
5
<option name="enableFeature" value="false" />
6
<option name="myCustomKey" value="value" />
7
</component>
8
</project>

In the same way, the name attribute in option here corresponds to the three attributes in MyState.java. These attributes should not be obfuscated; if they are, the original data will not be read. For example, if you set:

  • currentVersion: v1.0.1
  • enableFeature:false
  • myCustomKey:value

After obfuscation, assume the following properties in MyState.java:

  • currentVersion -> a
  • enableFeature -> d
  • myCustomKey -> c

The data in state.xml cannot be read at this point. Because there is no option name is: a, d, c data, so the final value read out is the default value defined in MyState.java, once this value has changed, will be in the xml again to add a new attribute. name is the variable name after the obfuscation.

Now the currentVersion has been obfuscated to a, so if the value of currentVersion changes, change it to v1.0.2, then state.xml will look like this

1
<?xml version="1.0" encoding="UTF-8"?>
2
<project version="4">
3
<component name="com.obiscr.template.MyState">
4
<option name="currentVersion" value="v1.0.1" />
5
<option name="enableFeature" value="false" />
6
<option name="myCustomKey" value="value" />
7
<!-- Added -->
8
<option name="a" value="v1.0.2" />
9
</component>
10
</project>

A <option name="a" value="v1.0.2" /> will be added. So as long as the variable name is not the same after obfuscation, it will keep adding. So here’s a way to fix it:

1
register<proguard.gradle.ProGuardTask>("proguard") {
2
...
3
4
keep("""
5
class * implements com.intellij.openapi.components.PersistentStateComponent {*;}
6
""".trimIndent()
7
)
8
...
9
}

This rule ignores all classes that implement PersistentStateComponent.

Summaries

This article briefly describes how to use ProGuard and obfuscate the IntlliJ plugin and integrate it into a gradle task.

Actually, I prefer to call it a framework. The specific obfuscation rules will need to be customised for your own project. I hope it can help you.

Disclaimer

This is just a demo project for research and study, please use it in your project at your own discretion.

I will not be responsible for any kind of damage caused by using this obfuscation scheme.

Tweet this article