跳转到内容

使用 ProGuard 混淆你的插件

插件混淆已经在官方文档中详细描述,请参阅:obfuscate the plugin

所以,本文将向你展示一个逐步演示,使用 ProGuard 混淆你的插件,从真实示例开始。

要查看比较文件,ProGuard 混淆的关键代码已添加到此 commit,所有代码都在此 repository 中。

build.gradle.kts 中,添加 buildscript 节点以导入 proguard 依赖项

buildscript {
repositories {
maven {
setUrl("https://maven.aliyun.com/repository/public/")
setUrl("https://maven.aliyun.com/nexus/content/groups/public/")
setUrl("https://plugins.gradle.org/m2/")
setUrl("https://oss.sonatype.org/content/repositories/snapshots/")
}
mavenCentral()
gradlePluginPortal()
}
dependencies {
classpath("com.guardsquare:proguard-gradle:7.3.2")
}
}

build.gradle.kts 中注册一个新 task

// Register a new task, task name is "proguard"
register<proguard.gradle.ProGuardTask>("proguard") {
dependsOn(instrumentedJar)
verbose()
val javaHome = System.getProperty("java.home")
File("$javaHome/jmods/").listFiles()!!.forEach { libraryjars(it.absolutePath)}
// Use the jar task output as a input jar. This will automatically add the necessary task dependency.
injars("build/libs/instrumented-${properties("pluginName")}-${properties("pluginVersion")}.jar")
outjars("build/obfuscated/output/instrumented-${properties("pluginName")}-${properties("pluginVersion")}.jar")
libraryjars(configurations.compileClasspath.get())
dontshrink()
dontoptimize()
adaptclassstrings("**.xml")
adaptresourcefilecontents("**.xml")
// Allow methods with the same signature, except for the return type,
// to get the same obfuscation name.
overloadaggressively()
// Put all obfuscated classes into the nameless root package.
repackageclasses("")
dontwarn()
printmapping("build/obfuscated/output/${properties("pluginName")}-${properties("pluginVersion")}-ProGuard-ChangeLog.txt")
target(properties("pluginVersion"))
adaptresourcefilenames()
optimizationpasses(9)
allowaccessmodification()
keepattributes("Exceptions,InnerClasses,Signature,Deprecated,SourceFile,LineNumberTable,*Annotation*,EnclosingMethod")
keep("""
class * implements com.intellij.openapi.components.PersistentStateComponent {*;}
""".trimIndent()
)
keepclassmembers("""
class * {public static ** INSTANCE;}
""".trimIndent()
)
keep("class com.intellij.util.* {*;}")
}
prepareSandbox {
if (properties("enableProGuard").toBoolean()) {
dependsOn("proguard")
pluginJar.set(File("build/obfuscated/output/instrumented-${properties("pluginName")}-${properties("pluginVersion")}.jar"))
}
}

配置完成后,运行插件,调试消息如下:

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

混淆后:

左侧是文件名已混淆,右侧是文件类名、方法名和变量名已混淆。

在 IntelliJ 插件项目中,有一些文件不需要混淆。本演示中列出了两个案例。

项目中的一个文件是 MyDefaultFileType.java。这个文件添加了一个新的文件类型,我们称之为 J-file,具有 *.j*.J 文件扩展名。

这个文件已经在 plugin.xml 中注册

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

关键点是 fieldName 属性:INSTANCE,它对应于 MyDefaultFileType.java 中的 INSTANCE。换句话说,它们必须相同。否则,当反射调用时,属性将找不到。

例如,一旦我们删除了 proguard task 的

register<proguard.gradle.ProGuardTask>("proguard") {
...
// keepclassmembers("""
// class * {public static ** INSTANCE;}
// """.trimIndent()
// )
...
}

运行插件并获取错误:

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

这表明 INSTANCE 属性找不到。因此,对于这种情况,你可以使用 proguard 的 keepclassmembers 来保持 INSTANCE 属性在所有类中不被混淆。当然,如果你愿意,你也可以保持 INSTANCE 在所有实现 INativeFileType 的类中不被混淆。这是一种更精确的控制。

你可能想知道,既然这个问题可以通过在 plugin.xml 中将 fieldName 与 MyDefaultFileType.java 中的 INSTANCE 保持相同来避免,为什么不通过将它改为相同的值来避免这个问题?

是的,这在原则上是可以的。然而,混淆每次都会生成一个随机变量名。也就是说,属性 fieldName 必须首先被指定为某个确切的值,然后开始打包,在打包后的类文件中,INSTANCE 不一定是上面指定的值。因此,我们仍然需要从混淆规则中排除 INSTANCE 以避免这个问题。

在许多情况下,需要存储一些本地数据,例如设置数据、环境数据等。在这种情况下,你需要使用 @State 来指定一个文件来存储数据。在这种情况下,你需要使用 @State 来指定存储文件。例如,

MyState.java
package com.obiscr.template;
import com.intellij.openapi.components.*;
import com.intellij.openapi.project.Project;
import com.intellij.util.xmlb.XmlSerializerUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@State( name = "com.obiscr.template.MyState", storages = @Storage("my-state/state.xml"))
public class MyState implements PersistentStateComponent<MyState> {
public String currentVersion = "";
public Boolean enableFeature = true;
public String myCustomKey = "";
public static MyState getInstance(@NotNull Project project) {
return project.getService(MyState.class);
}
@Nullable
@Override
public MyState getState() {
return this;
}
@Override
public void loadState(@NotNull MyState state) {
XmlSerializerUtil.copyBean(state, this);
}
}

这里定义了一个存储文件和一些属性,它将在项目 .idea 目录的 my-state 目录中创建一个新 state.xml 文件,内容如下

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

同样,这里的 option 中的 name 属性对应于 MyState.java 中的三个属性。这些属性不应该被混淆;如果它们被混淆,原始数据将不会被读取。例如,如果你设置:

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

混淆后,假设 MyState.java 中有以下属性:

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

state.xml 中的数据无法读取。因为 option 中没有 name 是:a, d, c 的数据,所以最终读取出的值是 MyState.java 中定义的默认值,一旦这个值发生变化,将在 xml 中再次添加一个新属性。name 是混淆后的变量名。

现在 currentVersion 已经被混淆为 a,所以如果 currentVersion 的值发生变化,将其更改为 v1.0.2,那么 state.xml 将如下所示

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

将添加一个 <option name="a" value="v1.0.2" />。因此,只要混淆后的变量名不同,它就会继续添加。所以这里有一个解决方法:

register<proguard.gradle.ProGuardTask>("proguard") {
...
keep("""
class * implements com.intellij.openapi.components.PersistentStateComponent {*;}
""".trimIndent()
)
...
}

这个规则忽略所有实现 PersistentStateComponent 的类。

本文简要介绍了如何使用 ProGuard 混淆 IntlliJ 插件并将其集成到 gradle 任务中。

实际上,我更喜欢称之为一个框架。具体的混淆规则需要根据你自己的项目进行定制。我希望它能帮助你。

这只是一个研究项目,请根据你自己的项目使用。

我不会对使用这种混淆方案造成的任何损害负责。